เขียน Node.js รันใน Docker Container ตั้งแต่ Zero to Hero

ความเดิมจากบทความที่แล้วตอน สร้าง RESTful API ด้วย Node.js + Express เบื้องต้น ไปแล้วนั้น จะเห็นว่าตอนท้ายของบทความ ได้แถมเรื่องการเขียน Dockerfile สำหรับนำ Code ของเรา ไป Build เป็น Docker image แต่ในความเป็นจริงแล้ว เราสามารถใช้ Docker มาเป็น Environment ในการพัฒนาได้ นั่นก็หมายความว่า เราไม่จำเป็นต้องติดตั้ง Node.js ในเครื่องของเราเลย ขอแค่ Pull image ของ Node.js ลงมาแค่นั้น ก็เพียงพอแล้ว

บทความนี้จึงจะพาย้อนไปที่จุดเริ่มต้นของการพัฒนา RESTful API ด้วย Node.js ด้วย Docker ในสไตล์ From Zero to Hero ถ้าใครพร้อมแล้ว ไปลุยกันเลยยยยย

ก่อนอื่นอย่าลืมติดตั้ง Docker กันก่อนนะครับ จะได้ทำไปด้วยกัน และสำหรับใครที่ติดตั้ง Docker Toolbox แนะนำให้อ่านบทความตอนทำความเข้าใจประตูมิติบน Docker Toolbox และเทคนิคเล็กน้อยกันก่อนนะครับ

เอาล่ะ ถ้าพร้อมแล้ว เรามาเริ่มกันที่ Docker Image สำหรับ Node.js กันก่อน…

 

เลือก Node.js docker image

ถ้าเราเข้าไปใน Docker Hub แล้วเซิจคำว่า node จะเจอ Official Repository อยู่ ซึ่งจะเห็นว่า มีให้เลือกมากมายหลาย Tag เห็นแล้วชวนให้งงกันเลยทีเดียว ส่วนในกล่อง Docker Pull Command เขียนไว้ว่า docker pull node ซึ่งหากเราใช้ Command นี้ จะเป็นการ Pull Image ที่มี Tag ว่า latest มาให้

Node.js on Docker Hub

แต่เดี๋ยวก่อน… ให้ดูในรูปข้างบนนะครับ กรอบสีเขียว คือชื่อ Tag ในตระกูล Alpine Linux ซึ่งเป็น Linux ที่มีขนาดเล็กมาก ๆ ส่งผลให้ Docker image ที่สร้างมาจาก Alpine Linux มีขนาดเล็กตามไปด้วย ทำให้เราใช้เวลาในการ Pull image ไม่นาน และไม่เปลือง Internet

ส่วนในกรอบสีเหลืองนั้น จะเป็น Tag ในตระกูล Slim ซึ่งสร้างมาจาก Debian Linux ที่ถูกตัดแต่งให้มีขนาดเล็ก แต่ก็ยังสู้ Alpine ไม่ได้

และสำหรับในกรอบสีส้ม จะเป็นเลข Version เพียว ๆ รวมถึง latest ด้วย สร้างมาจาก Debian Linux เช่นกัน แต่จะมีขนาดใหญ่มาก รวมถึงตระกูล stretch และ wheezy ก็เช่นกัน (ทั้งสองชื่อเป็น Code name ของ Debian แต่ละ Version)

สุดท้าย ตระกูล onbuild อันนี้ทีมงานประกาศ Deprecate ไปแล้วนะครับ ก็อย่าไปใช้มัน

แนะนำว่า ให้เลือกตระกูล Alpine ไว้ก่อน ถ้าชอบฟีเจอร์ใหม่ ๆ ของ Node.js ก็เลือกเวอร์ชั่นล่าสุดไปเลย

และในแต่ละแถวนั้น จะมีเลข Version ทั้งแบบย่อ และแบบเต็ม แนะนำให้เลือกแบบเต็มไว้ก่อน เพื่อเป็นการเจาะจง Version และทำให้การ Pull Image ด้วย Tag เดิม มักจะได้ Image ตัวเดิมเสมอ (โอกาสที่ Image ที่สร้างมาเป็น Tag เดิมมีน้อยมาก)

ถ้าจากรูปบน Tag ที่น่าสนใจสุดก็คือ 8.4.0-alpine รองมาก็ 8.4.0-slim นั่นเอง (งานจริง ๆ ผมก็ใช้อยู่ 2 ตัวนี้ล่ะ)

เพราะฉะนั้น เรามา Pull image กันก่อน ด้วยคำสั่งด้านล่างนี้กันเลย

รอ Download ซักพัก ก็จะได้ Image ของ Node.js มาแล้ววววว

 

สร้าง Container สำหรับ Dev – Part 1

เมื่อ Pull image มาเรียบร้อยแล้ว อย่าลืมตรวจสอบว่า Image ได้อยู่ในเครื่องเราแล้ว ด้วยคำสั่ง docker images ซึ่งก็จะต้องเห็น Image ที่เรา Pull มา ดังนี้

Docker images command

สำหรับคำสั่งในการสร้าง Container มี Syntax ดังนี้

เราก็มาสร้าง Container แรกกัน ด้วยคำสั่ง

Tip #1 : หากเราไม่ได้ Pull image มาก่อน การสั่ง docker run Image ที่ไม่เคย Pull มันจะทำการ Pull ให้ก่อน

Tip #2 : การใส่ \ ไว้ท้ายบรรทัด คือการบอก Shell ว่า ยังไม่จบคำสั่ง บทความนี้เลือกใส่ \ ไว้ เพื่อให้อ่านง่าย การทำงานจริงสามารถพิมพ์คำสั่งยาว ๆ ต่อกันไปได้เลย

จากคำสั่งด้านบน Options ของคำสั่ง docker run มีอยู่ด้วยกัน  3 ตัว คือ -it มาจาก -i (Interactive) และ -t (TTY) ซึ่งเมื่อใส่ 2 ตัวนี้แล้ว จะทำให้เราสามารถสั่งงานและแสดงผล Shell ใน Container ได้

ส่วนอีก Option คือ --name <Container Name> อันนี้ไว้ตั้งชื่อ Container (ถ้าไม่ตั้งชื่อ Docker จะตั้งชื่อให้เราเอง)

ส่วน node:8.4.0-alpine คือ Docker image ที่เราต้องการใช้งาน และ sh ข้างท้าย คือ Command ที่เราต้องการให้เป็น Process ของ Container ที่จะสร้าง (ถ้า Command นี้ทำงานเสร็จแล้ว Container จะหยุดทำงานตามไปด้วย)

เมื่อเราให้ sh (Shell) เป็น Process ของ Container ชื่อ node-dev1 จึงทำให้ Terminal เรา ไปอยู่ที่ Shell ใน Container พร้อมรับคำสั่งต่อไปจากเราแล้ว (อย่าลืม Option -it)

สร้าง Docker container สำหรับพัฒนา Application ด้วย node.js

จากรูปด้านบน จะเห็นว่า บน Terminal เปลี่ยนจาก Shell บน macOS ไปเป็น Shell ของ Container แล้ว และสำหรับ Image จาก node จะรันด้วย User root ด้วย (สังเกตุที่ Prompt เป็น #)

และผมได้ลองสั่ง cat /etc/hostname เพื่อดูชื่อ Host ของ Container จะเห็นว่า ชื่อ Host จะเป็นเลขฐาน 16 ยาว 12 ตัว จดเลขนี้กันไว้ก่อนนะ…

ลองเปิดอีก Terminal นึงขึ้นมา แล้วพิมพ์คำสั่ง docker ps -a เพื่อดู Container ทั้งหมดที่ถูกสร้างขึ้นมาแล้ว

List ดูรายชื่อ Docker container ทั้งหมด

ผลลัพธ์จะเหมือนในกรอบสีเขียว ใช่มั๊ยครับ สังเกตุที่ CONTAINER ID จะเห็นว่า เป็นตัวเดียวกับชื่อ Host ใน Container ที่เราเพิ่งจะสร้างมานั่นเอง ซึ่งเราสามารถใช้ Container ID ไว้อ้างอิงกับคำสั่งอื่น ๆ ได้ และที่ NAMES จะเป็นชื่อ Container ที่เราตั้งไว้เอง ชื่อนี้ก็ไว้อ้างอิงได้เช่นกัน

ที่ COMMAND จะเห็นว่าเป็น sh ทำให้เรารู้ว่า Container นี้ ผูก Process กับ Shell นั่นเอง ส่วนที่ STATUS จะบอกสถานะว่า Container นี้ รันขึ้นมาได้ 10 วินาทีแล้ว

กลับไปที่ Terminal เดิม เราอยู่ที่ Shell ใน Container ใช่มั๊ยครับ ให้ออกจาก Container ด้วยการสั่ง exit ก็จะกลับมายัง Shell ของ OS เราละ แล้วลอง docker ps -a ดูครับ จะเห็นว่าผลลัพธ์จะเหมือนกรอบสีเหลือง ดังรูปข้างบน สังเกตุที่ STATUS จะเห็นว่า สถานะเป็น Exited ไปแล้ว ก็คือ Container นี้ ไม่ได้ทำงานแล้ว

สิ่งที่เกิดขึ้นก็คือ Process ของ Container นั้น ได้ผูกไว้กับ sh ตอนเราสั่ง sh มันจึงรอคำสั่งต่อไปจากเรา ภายใน Container แต่พอเราสั่ง exit ใน Container ก็คือเราจะออกจาก sh ทำให้ Process ที่ผูกไว้กับ Container สิ้นสุดลง Container จึงหยุดทำงานไปด้วย

ทีนี้ เรามาลองสั่งให้ Container เริ่มต้นกลับมาทำงานใหม่อีกครั้ง ด้วยคำสั่ง

แล้วลองตรวจสอบ Container ดูครับ จะเห็นว่า สถานะกลับมาเป็น Up แล้ว แต่สิ่งที่แตกต่างอย่างหนึ่งก็คือ เราไม่ได้อยู่ที่ Shell ใน Container

หากเราต้องการสั่งคำสั่งอะไรภายใน Container จะมี Syntax ดังนี้

เช่นถ้าเราอยากสั่ง node -v เพื่อดู Version ของ Node.js เราก็บอก Command ให้กับ Container ว่า node และบอก Argument ว่าเป็น -v นั่นเอง

รันคำสั่งที่ต้องการใน Container จากภายนอก

ถ้าต้องการรัน sh ใน Container อีกครั้ง ก็จะใช้คำสั่ง

เราก็จะเข้าไปยัง Shell ใน Container อีกครั้ง และถ้าลองออกจาก Container มา แล้วตรวจสอบ Container ดู จะเห็นว่า Container ไม่ได้หยุดการทำงาน เพราะว่าการสั่ง docker exec ไปยัง Container ด้วย command sh นั้น เป็นการเปิด Shell ขึ้นมาใน Session ใหม่ ไม่ได้ใช้ Session ที่ผูกไว้กับ Container

ตอนนี้ถ้าใครออกมาจาก Container แล้ว ให้กลับเข้าไปใหม่นะครับ

ทีนี้ ให้ทุกคนสร้าง Directory /app (mkdir /app) และเข้าไปยัง Directory cd /app แล้วทำการสร้างไฟล์ server.js ขึ้นมา และใส่ Code ดังนี้

สำหรับใครที่ไม่ถนัดคำสั่ง Linux ในการสร้างไฟล์ หรือไม่ถนัด Text Editor บน Linux เช่น vi ให้ใช้คำสั่งด้านล่างนี้ครับ สามารถสร้างไฟล์ server.js และใส่ Code ด้านบนไว้ให้ได้เหมือนกัน

เมื่อสร้างไฟล์ server.js แล้ว มาลองรันด้วย node กันเลยยยยย

รัน Node.js ใน Docker Container

จะเห็นว่า เราสามารถที่จะใช้ Node.js ภายใน Docker ทำงานได้แล้ว โดยไม่ต้องติดตั้ง Node.js ในเครื่องของเราเลย

ตอนนี้เราได้ Environment สำหรับ Node.js ใน Docker แล้ว แต่มันไม่สะดวกต่อการทำงานเอาซะเลย จริงมั๊ยครับ ในชีวิตจริง น้อยคนนักที่จะใช้ vi ภายใน Container ในการเขียน Code นอกจากนี้แล้ว ไฟล์ที่เราสร้าง ก็จะอยู่ใน Container หากเราลบ Container ทิ้ง ไฟล์ภายใน Container ก็จะหายไปด้วย

ชีวิต Dev ควรง่ายกว่านี้ เพราะฉะนั้น ไปดูหัวข้อต่อไปกันต่อเลย…

 

สร้าง Container สำหรับ Dev – Part 2

หัวข้อนี้ จะแนะนำให้รู้จักการ Bind volume หรือการนำไฟล์บนเครื่องของเรา ให้มองเห็นจากใน Container

จากหัวข้อที่แล้ว ให้สร้างไฟล์ server.js ขึ้นมาอีกครั้ง โดยวางใน Folder บนเครื่องของเราขึ้นมาก่อน แล้วรันคำสั่งดังนี้ จาก Terminal

การ Bind Volume เข้าไปยัง Container

จากคำสั่งด้านบน "$(pwd)" จะเป็นคำสั่งที่ใช้ในการหา Path ปัจจุบัน ที่เราทำการป้อนคำสั่งอยู่ (ตรงนี้เป็น Unix command ยังไม่เกี่ยวกับ Docker ใด ๆ)

สำหรับ Option -v "$(pwd)":/app คือการ Bind volume จาก Path ปัจจุบัน ไปยัง Path /app ภายใน Container คั่นระหว่าง Path บนเครื่องเรา กับ Path ใน Container ด้วย : ทำให้ไฟล์ทุกไฟล์ใน Folder ปัจจุบัน มองเห็นจาก Path /app ใน Container แล้ว (หากต้องการ Bind หลาย ๆ ชุด ก็สามารถใส่ -v เพิ่มได้อีก)

ส่วน Option -w /app เป็นการเปลี่ยน Working Directory ภายใน Container คำสั่งต่าง ๆ จะเริ่มต้นที่ Path นี้ ในที่นี้เราจึงเปลี่ยนไปที่ Path /app ซึ่งเป็นที่เดียวกันกับที่เรา Bind volume เข้าไป

ส่วน Command ที่จะใช้เป็น Process ของ Container ในครั้งนี้ จะเป็น node server.js ก็คือการให้ node รันไฟล์ server.js ที่ path /app ที่มาจากการ Bind volume นั่นเอง

ถ้าลองตรวจสอบ Container ด้วยคำสั่ง docker ps -a ดู จะพบว่า สถานะของ Container นั้น Exit ไปแล้ว เนื่องจาก Command ที่ผูก Process กับ Container นั้น ทำงานเสร็จสิ้นไปแล้ว

หากต้องการรันคำสั่งเดิมอีกครั้ง จำเป็นต้องลบ Container ก่อน หรือเปลี่ยนชื่อ Container ซึ่งไม่สะดวก เราสามารถนำ Container มารันคำสั่งเดิมซ้ำ ด้วยคำสั่ง

Option -a คือการแสดง Output ภายใน Container ออกมาให้ดู

หากทดลองแก้ไขไฟล์ server.js ดู ก็จะเห็นว่า เมื่อสั่ง docker start ใหม่ ก็จะทำให้ Container รันคำสั่ง node server.js ใหม่ เราจึงเห็นผลลัพธ์ใหม่แล้ว สะดวกขึ้นเยอะเลย

ทีนี้ลองเปลี่ยน Code ในไฟล์ server.js อีกครั้ง โดยครั้งนี้จะให้ทดลองสร้าง app ที่ Listen Port 3000 จากตัวอย่างที่หน้า About Node.js และเปลี่ยน IP ของ hostname เป็น 0.0.0.0 ดังนี้

ที่ต้องเปลี่ยน เพราะถ้าใช้ IP 127.0.0.1 จะทำให้รับ Request ได้จากเฉพาะใน Container เท่านั้น

เมื่อแก้ไขไฟล์เสร็จ ทดลองรันด้วยคำสั่ง

หลังจากนั้น ลองตรวจสอบ Container ดู จะพบว่า Container นี้ จะยังรันอยู่ เนื่องจาก Process ของ node ยังไม่จบ (เพราะรอ Listen อยู่ที่ Port 3000 นั่นเอง)

ลองทดสอบเรียกใช้งานด้วยคำสั่ง telnet localhost 3000 ใน Container เพื่อทดสอบการทำงานของ app ดังนี้

เมื่อเราสั่ง telnet ใน Container สำเร็จ คำสั่ง telnet จะรอให้เราป้อนคำสั่งที่ต้องการ Request ไป ในรูปด้านล่าง ทดลอง Request ไปด้วย HTTP GET ด้วยคำสั่ง GET / (อย่าลืม Enter 2 ครั้ง เพื่อบอก Telnet ว่าสิ้นสุดคำสั่งร้องขอแล้ว) จะได้ผลดังรูป

ทดสอบ Node.js ที่เป็น Server App ใน Docker Container

จะเห็นว่า app ที่เขียนด้วย Node.js เป็น Server App สามารถทำงานอยู่ภายใน Container ได้ แต่ยังไม่ค่อยสะดวกในการทดสอบจากภายนอก Container เท่าไหร่ เราจึงจำเป็นต้องทำการ Publish port จากภายใน Container ออกมาภายนอก

เนื่องจากจนถึงตอนนี้ คำสั่ง Docker ยังไม่สามารถทำการ Publish port จาก Container ที่สร้างขึ้นมาแล้วได้ จึงจำเป็นต้องสร้าง Container ขึ้นมาใหม่ โดยใช้ Option -p ของคำสั่ง docker run ในการ Publish port ดังนี้

การ Publish port กับ Container

จากคำสั่งด้านบน Option -d ใช้สำหรับให้ผลการรันคำสั่งนี้อยู่ในเบื้องหลัง (เราจะไม่เห็นผลการรันของ Container) ส่วน Option -p 4000:3000 เป็นการ Publish port 3000 จากใน Container ออกมาข้างนอกเป็น Port 4000 เท่ากับว่า ณ ตอนนี้ บนเครื่องเรา เปิด Listen ที่ Port 4000 ไว้ ถ้าเรียกใช้งานผ่าน localhost:4000 ก็จะเป็นการ Request ไปยัง Port 3000 ใน Container นั้นเอง ชีวิตง่ายขึ้นไปอีกนิด…

ทดสอบ Server app บน Browser

หากต้องการดูผลการรันคำสั่งภายใน Container สามารถใช้คำสั่ง docker logs ดูได้ มี Syntax ดังนี้

ตัวอย่าง

ก่อนจะไปหัวข้อต่อไป ขอทำลาย Container 3 ตัว ที่สร้างมาทั้งหมดก่อนนะ ไม่ได้ใช้แล้ว โดยคำสั่งลบ Container มี Syntax ดังนี้

ลบ Container

จากรูปด้านบน ผมใช้ Option -f ใช้สำหรับลบ Container ที่รันอยู่ได้ โดยไม่ต้อง Stop ก่อน ส่วนรายชื่อ Container ที่เห็น สามารถระบุเป็นชื่อ Container หรือ Container ID หรือจะเป็น Container ID แบบย่อ ก็ได้ผลเหมือนกัน (ในรูป Container ID ตัวสุดท้าย คือ 1 ก็คือ Container ที่ชื่อ node-dev3 นั่นเอง

 

เตรียม Dev Environment ด้วย Container

สำหรับ 2 หัวข้อบน ยังไม่เพียงพอสำหรับ Dev ในการทำงาน เนื่องจาก app ที่เขียนด้วย Node.js มักจะใช้ Dependency จากภายนอก ส่วนใหญ่จะใช้คำสั่ง npm init และ npm install ในการสร้าง Project และติดตั้ง Dependency ในหัวข้อนี้ เราจะมาใช้ npm ใน Container จัดการ Environment กัน

เริ่มโดยการสร้าง Node.js Project โดยปกติเราจะใช้คำสั่ง npm init เราก็จะใช้คำสั่งนี้ รันใน Container และ Bind volume ไฟล์ออกมาข้างนอก ดังนี้

สร้าง Node Project ด้วย Container

จากรูปด้านบน ในกรอบสีเหลือง คือผลการทำงานใน Container

มี Option ใหม่มาอีกแล้ว สำหรับคำสั่ง docker run ก็คือ --rm สำหรับ Option นี้ เมื่อ Container ทำงานเสร็จ จะทำลายตัวเองทิ้งทันที ผมจึงไม่ใส่ Option --name เพื่อตั้งชื่อให้มัน

จากคำสั่งด้านบน เราก็สามารถใช้คำสั่ง npm init สร้างไฟล์ package.json ขึ้นมาภายใน Container แล้ว ซึ่งเราก็ทำการ Bind volume ไว้ด้วย ทำให้เราได้ไฟล์ package.json มาอยู่บนเครื่องเราเรียบร้อย

ต่อไป มาลองติดตั้ง Dependency กันบ้าง เอา Express ละกันเน๊าะ ปกติเราก็จะใช้คำสั่ง npm install express --save พอสั่งรันใน Container ก็จะเป็น

เรียบร้อยครับ เราก็จะได้ Folder node_modules มาแล้ว

ถ้าเราต้องใช้คำสั่ง npm หลาย ๆ คำสั่ง การสร้าง ๆ ลบ ๆ Container บ่อย ๆ อาจจะรู้สึกขัดใจ เราก็สามารถรัน sh แทน แล้วค่อยสั่ง Command ต่าง ๆ บน sh ต่อก็ได้

สร้าง Node Project ด้วย Container

เมื่อเราได้ Environment สำหรับ Dev เรียบร้อย ก็ใช้ Command เดียวกับหัวข้อที่แล้ว เพื่อสร้าง Container ขึ้นมาเพื่อใช้ Dev ต่อได้เลยยยยยย

คำสั่งด้านบน ผมเปลี่ยนการ Publish port ออกมา ให้เลขตรงกันละ

ทีนี้พอมีการแก้ไข Code เราก็สั่งให้ Container ทำการ Restart ใหม่ ดังนี้

ตอนนี้เราก็จะได้ Dev Environment ด้วย Container ที่สะดวกต่อการพัฒนา app แล้วล่ะ

แต่ว่า ถ้าเราต้องสร้าง Environment ใหม่บ่อย ๆ การมาป้อนคำสั่ง npm install บ่อย ๆ ต้องเสียเวลาในการติดตั้ง Dependency ใหม่ทุกครั้ง และยิ่งถ้างานนี้ทำกันเป็นทีม ยิ่งไม่สะดวกเท่าไหร่ เพราะฉะนั้น เราควรเตรียม Environment ที่สามารถสร้างใหม่เมื่อไหร่ก็ได้ ให้สะดวกกว่านี้ขึ้นไปอีก ด้วยการสร้าง Docker Image ไว้เลย

 

สร้าง Image เป็น Dev Environment

สำหรับการสร้าง Docker Image เพื่อเป็น Dev Environment นั้น เราจะต้องเขียน Dockerfile โดยนำคำสั่งที่ต้องการติดตั้ง Dependency ให้รันตอนสร้าง Image

ก่อนอื่น ขอให้ลบ Folder node_modules ทิ้งไปก่อน เราจะใช้ไฟล์ package.json ที่มีการประกาศ Dependency ไว้แล้ว (จากหัวข้อที่แล้ว) ในการสร้าง Image เป็น Dev Environment

และขอเปลี่ยนโครงสร้างการวางไฟล์นิดนึง โดยสร้าง Folder src แล้วย้ายไฟล์ server.js ไปไว้ข้างไหน และแก้ไขไฟล์ package.json ดังนี้

สำหรับการสร้าง Docker Image เริ่มจากการสร้างไฟล์ชื่อ Dockerfile ดังนี้

คำสั่ง FROM ไว้กำหนดว่า Image ที่เราจะสร้าง ใช้ Image ไหนมาเป็นฐาน

คำสั่ง WORKDIR ไว้กำหนดว่าจะใช้ Path ไหนเป็น Working Directory ในตัวอย่างกำหนดเป็น /app

คำสั่ง COPY ไว้นำไฟล์จากเครื่องของเรา ในตัวอย่าง กำหนดให้นำไฟล์จาก Current Path (.) ไปไว้ที่ /app

คำสั่ง RUN เป็นการสั่ง Command ที่ต้องการรัน ในที่นี้เราสั่ง npm install เพื่อที่จะให้ npm ติดตั้ง Dependency ที่เราประกาศไว้ในไฟล์ package.json ติดตั้งใน Image ที่เรากำลังจะสร้าง

อีกหนึ่งไฟล์ที่แนะนำให้สร้างไว้ก็คือ .dockerignore (สำหรับใครที่ใช้ git คงจะรู้จักไฟล์ .gitignore ใช่แล้วครับ มันเหมือนกันเลย) ไฟล์นี้ไว้ประกาศว่า ไฟล์ใดบ้าง ที่เราไม่ต้องการให้ Docker เห็น เมื่อตอนเรา Build Image

แน่นอนว่า ไฟล์ .dockerignore เอง และ Dockerfile ก็เป็นไฟล์ที่ไม่ได้ต้องการใช้ใน Image รวมถึง Folder node_modules ด้วย เพราะเราจะให้ Docker ทำการรัน npm install เพื่อติดตั้ง Dependency ใหม่อยู่แล้ว

ปกติผมก็จะสร้างไฟล์ .dockerignore สำหรับ Node.js app ประมาณนี้

เมื่อเตรียมไฟล์ Dockerfile และ .dockerignore เรียบร้อย พิมพ์คำสั่งต่อไปนี้ เพื่อสร้าง Docker Image

สร้าง Docker Image

คำสั่งด้านบน จะทำการสร้าง Docker Image ขึ้นมาใหม่ ด้วยคำสั่ง build โดย Option -t docker-node:dev จะเป็นการบอก Docker ว่า เมื่อ Build Image เสร็จแล้ว ให้กำหนดชื่อ Image เป็น docker-node และมี Tag ว่า dev (ถ้าไม่ได้กำหนด จะได้เป็น latest ส่วน . คือตำแหน่งของ Dockerfile ในที่นี้หมายถึง Path ปัจจุบัน

ลองใช้คำสั่ง docker images ก็จะเห็น Image ที่สร้างขึ้นแล้ว

 

สร้าง Container สำหรับ Dev จาก Dev Image

จากหัวข้อที่แล้ว เราจะได้ Dev Image มา ในชื่อ docker-node:dev แล้ว หากเราต้องเปลี่ยนแปลง Code หากเราต้อง Build Image ใหม่ จะเสียเวลามาก เพราะต้องรอคำสั่ง npm install ในขั้นการสร้าง Image ทำงานใหม่ทุกครั้ง ซึ่งเราสามารถเลี่ยงได้ 2 วิธี คือ

  1. เปลี่ยนคำสั่งใน Dockerfile ให้ทำการ Copy เฉพาะไฟล์ package.json เข้าไปก่อน แล้วรัน npm install แล้วค่อย Copy ไฟล์อื่นเข้าไปทีหลัง แบบนี้ Docker จะเห็นว่าในการสั่ง npm install จะเหมือนครั้งก่อน ๆ ก็จะหยิบ Cache มาใช้เลย แต่ข้อเสียคือ ทำให้ Layer ของ Docker Image เพิ่มขึ้น
  2. ใช้ Dev Image มาสร้าง Container แล้ว Bind volume เฉพาะไฟล์ส่วนที่ต้องการแก้ไข

ในหัวข้อนี้ จะแนะนำวิธีที่ 2 เนื่องจากวิธีที่ 1 ไม่แนะนำเพราะเรื่องของ Layer ใน Docker Image ที่สร้างมา จะเพิ่มมากขึ้น

สำหรับการสร้าง Container เราจะใช้คำสั่งประมาณนี้

จุดที่น่าสนใจของคำสั่งด้านบน อยู่ที่บรรทัดที่ 4 เราจะเปลี่ยนการ Bind volume เฉพาะ Folder src เข้าไปใน Container ทำให้ไฟล์ package.json และ Folder node_modules จากเครื่องเรา ไม่ถูก Bind เข้าไปใน Container ทับของที่สร้างมาจาก Dev Image ที่เราสร้างไว้แล้ว

อีกจุดคือ เราไม่ต้องใส่ -w เพื่อบอก Working Directory แล้ว เพราะเราประกาศไว้ในขั้นตอน Build Image ไปแล้ว

ทีนี้เราก็สามารถที่จะแก้ไขไฟล์ภายใน Folder src แล้วก็แค่ Restart Container ใหม่ Container ก็จะถูกรันด้วย Code ชุดใหม่ได้แล้ว

แต่เนื่องจากการสั่ง docker restart หรือแม้แต่ docker stop การสิ้นสุด Process นั้น จะ Exit ออกมาด้วย Code 137 (Process Kill) เนื่องจาก Node.js ไม่ได้ Handle signal สำหรับการสั่งสิ้นสุดการทำงาน จึงทำให้ Docker สั่ง Kill Process ภายใน Container ทิ้ง จึงทำให้การหยุดการทำงานของ Container นั้นช้า ไม่ทันใจชาว Dev เท่าไหร่

เนื่องจากใน package.json เรามีการประกาศ start script ไว้แล้ว เราจึงสามารถใช้คำสั่ง npm start แทนได้ และ npm มีการ Handle signal สำหรับสั่งสิ้นสุดการทำงานไว้ให้ด้วย ทำให้การทำงานนั้น เร็วกว่ามาก เพราะฉะนั้น เราจะเปลี่ยนคำสั่งด้านบนใหม่ ดังนี้ (อย่าลืมลบ Container ล่าสุดทิ้งก่อนนะ)

แต่ถ้าหากเราจำเป็นต้องสร้าง Dev Image ใหม่ ด้วยคำสั่ง docker build และนำมาสร้าง Container ด้วยคำสั่ง docker run บ่อย ๆ ก็คงไม่สนุกแน่ เราสามารถใช้ Docker Compose ทำการรวบทั้งการ Build และ Run ให้เหลืออยู่คำสั่งเดียวได้

ก่อนอื่น ให้สร้างไฟล์ docker-compose.yml ดังนี้

บรรทัดแรก เป็นการประกาศ Compose file version ซึ่งเวอร์ชั่นใหม่ ๆ ก็จะมีคำสั่งใหม่ ๆ ให้ใช้งาน

เราจะประกาศชื่อ Service ไว้ภายใต้คำสั่ง services ในตัวอย่างด้านบน ตั้งชื่อ Service ว่า node-express

สำหรับ Service node-express มีการกำหนดคุณสมบัติเพิ่มเติม โดยให้ทำการ build Docker Image โดยใช้ Dockerfile จาก Current Path (.) โดยให้ชื่อ Image เป็นชื่อ docker-node:dev และนำมาสร้าง Container (container_name) ชื่อ node-dev5 ด้วยคำสั่ง (command) npm start พร้อม Publish Port ไว้ด้วย

และสำหรับ volumes นั้น เป็นการ Bind Volume จาก Folder src ใน Path ปัจจุบัน เข้าไปยัง Path /app/src ภายใน Container

สังเกตุว่า ทั้งหมด ก็คือสิ่งที่เราทำมาแล้ว เพียงแต่ย้ายจากการใช้คำสั่ง พร้อม Option ต่าง ๆ มาเป็น docker-compose.yml

เมื่อได้ไฟล์ docker-compose.yml มาแล้ว เราจะนำมาใช้งานด้วยคำสั่งต่อไปนี้ (อย่าลืมลบ Container ล่าสุดทิ้งก่อนนะ)

ใช่แล้วครับ คำสั่งเหลือแค่นี้ล่ะ เราใช้คำสั่ง docker-compose up นั่นเอง โดยสำหรับ Option -d ก็จะเหมือนกับคำสั่ง docker run คือให้ทำงานอยู่เบื้องหลัง และ Option --build เพื่อสั่งให้ Build Image ก่อน

ใช้ Docker Compose สร้าง Docker Image และ Container

นอกจากนี้ เรายังสามารถใช้คำสั่ง docker-compose logs สำหรับดู Log ของ Service ทั้งหมด (Container ใน Docker Compose จะเรียกว่า Service)

คำสั่ง docker-compose ps เพื่อดูสถานะของ Service ทั้งหมด

คำสั่ง docker-compose restart ใช้ Restart Service ทั้งหมด

และคำสั่ง docker-compose down ใช้ลบ Service ทั้งหมด

คำสั่ง Docker Compose อื่น ๆ ที่ใช้บ่อย

ตอนนี้ Dev ก็สามารถใช้ Docker Compose ทำการ Build Dev Image ด้วยคำสั่งของ Docker Compose ร่วมกับไฟล์ Dockerfile และมีการ Bind Volume สำหรับไฟล์ที่ต้องการแก้ไข ซึ่งประกาศไว้ในไฟล์ docker-compose.yml และยังสามารถใช้คำสั่ง docker-compose restart ในการอัพเดตไฟล์ที่แก้ไข ไปยัง Container ได้สะดวกสบายแล้ว ชีวิต Dev ควรจะดีแบบนี้ล่ะ

 

สร้าง Production Image

เมื่องานเสร็จแล้ว ก็ถึงเวลาที่จะมาสร้าง Production Image กัน ก่อนอื่น แนะนำให้ทำการอัพเดต Dependency ของ Node.js ด้วยคำสั่ง npm update ก่อน ดังนี้

ถ้า Dependency มี Version ใหม่ จะถูกอัพเดตไปยังไฟล์ package.json ด้วย

ทดสอบงานให้เรียบร้อย แล้วทำการ Freeze dependency version ไว้ ด้วยการนำเครื่องหมาย ^ หน้าเลข Version ออก

สำหรับ Dockerfile ให้เพิ่มคำสั่ง ดังนี้

คำสั่ง EXPOSE ไว้ประกาศว่า Docker Image นี้ เมื่อสร้าง Container จะใช้ Port อะไรบ้าง

คำสั่ง USER ไว้เปลี่ยน User ใน Container เป็น User อื่น (Image ของ Node.js ได้เตรียม User ชื่อ node ไว้ให้แล้ว) ช่วยในเรื่อง Security ถ้าไม่เปลี่ยน ก็จะถูกรันด้วย root ตามที่เห็นในหัวข้อแรก ๆ

คำสั่ง CMD ไว้สำหรับกำหนดคำสั่ง Default ของ Container หากนำ Image ไปสร้าง โดยไม่ได้ระบุ Command ใด ๆ เพิ่มเติม

เนื่องจากใน Dockerfile มีการกำหนด CMD ไว้แล้ว ใน docker-compose.yml จึงไม่จำเป็นต้องสั่งคำสั่งซ้ำอีก

และในส่วนของชื่อ Image เราก็สามารถตั้งชื่อ Production Image ไว้ในไฟล์ docker-compose.yml ได้เลย

แล้วเราก็จะสร้าง Production Image กันด้วยคำสั่ง docker-compose build (จะใช้ docker build เหมือนเดิม ก็ได้เช่นกัน)

สร้าง Production Image ด้วย Docker Compose

หากต้องแก้ไข Code ตอนนี้ Dev ก็สามารถมี Environment สำหรับพัฒนา ที่คล่องตัวมาก และมั่นใจได้ว่าการ Ship Image ไปยัง Production Server จะไม่พบปัญหา เพราะเป็น Environment เดียวกันแล้ว

สำหรับการ Ship Image ไปยัง Production Server นั้น เมื่อเราได้ Docker Production Image มาแล้ว เราสามารถใช้คำสั่ง docker save เพื่อนำ Image ไปสร้างเป็นไฟล์ .tar แล้วใช้คำสั่ง docker load ในเครื่องที่จะ Deploy ก็ได้

หรือว่าจะใช้คำสั่ง docker push เพื่อนำ Image ไปเก็บไว้ที่ Docker Hub หรือ Docker Registry เพื่อไว้ Pull ที่เครื่องที่จะ Deploy ก็ได้เหมือนกัน (แนะนำให้ใช้ท่านี้ดีกว่า)

แต่ในทางปฏิบัติ แนะนำให้ใช้ระบบ Automate ในการนำ Code ของเราทั้งหมด ที่ Commit เข้าสู่ Git Server ไปทำการ Build Production Image ด้วย CI (Continuous Integration) และนำ Production Image ไปเก็บที่ Docker Hub/Registry ด้วย CD (Continuous Deployment) หรือจะสั่งให้ไป Deploy บน Production Server เลย ด้วย CD (Continuous Delivery) ก็ได้ Dev ก็เพียงแค่ Commit Code และ Push เข้า Git Server เท่านั้นเอง ชีวิตดี๊ย์ดี…

 

ส่งท้าย

สำหรับบทความนี้ เริ่มเขียนเมื่อต้นเดือนกันยายน แต่ก็ไม่ค่อยได้เขียนอย่างต่อเนื่อง เพราะภารกิจเยอะมว๊ากกกกก จนถึงตอนนี้ Node.js ก็ได้ขยับ Version ไปเป็น 8.7.0 แล้วล่ะ

ตอนนี้ก็ ตี 2 แล้ว ด้วยเวลาที่ไม่ค่อยจะว่าง เขียนเนื้อหาได้ไม่ค่อยจะต่อเนื่อง หากมีเนื้อหาตรงไหนผิดพลาด ตกหล่น หรืออ่านแล้วไม่เข้าใจ รบกวนผู้อ่าน เขียน Comment มาบอกกันซักนิดนึงนะครับ จะได้กลับมาแก้ไขให้คร๊าบบบบบบ

Leave a Reply

Your email address will not be published. Required fields are marked *