ติดตั้งด้วย Docker
วิธีที่เร็วที่สุดในการได้ MongoDB จริง ๆ มาเล่นคือ Docker image mongo ทางการให้ server ที่สะอาดและแยกตัวแก่คุณในคำสั่งเดียว โดยไม่มีอะไรติดตั้งถาวรบนเครื่องของคุณและไม่มีอะไรต้องถอนการติดตั้งทีหลัง บทเรียนนี้พาคุณจาก “Docker กำลังทำงานอยู่” ไปสู่ “ฉันคุยกับ MongoDB ได้” — เริ่มด้วยคำสั่งบรรทัดเดียว แล้วตามด้วย docker-compose.yml เล็ก ๆ ที่คุณเก็บไว้ในโปรเจกต์ได้ — และตอบคำถามที่โมดูลหลัง ๆ พึ่งพา: standalone หรือ replica set?
คุณต้องการแค่ Docker Desktop (หรือ Docker engine) ที่ติดตั้งและทำงานอยู่ ทุกคำสั่งด้านล่างรันจริงบน Mac เครื่องเดียวกับ mongo:8 = 8.3.11 (mongosh 2.11.1, Node driver 7.7.0) เพื่อให้ container ทดสอบเป็นระเบียบเราตั้งชื่อเป็น mongodb-b1-dev และอื่น ๆ ส่วนคำสั่งในบทนี้ใช้ชื่อ mongo-dev ที่เป็นมิตรกว่าซึ่งคุณควรใช้
standalone หรือ replica set? ตัดสินใจก่อน
หัวข้อที่มีชื่อว่า “standalone หรือ replica set? ตัดสินใจก่อน”container mongo ธรรมดาคือ server แบบ standalone ซึ่งเหมาะกับโมดูล 1 ถึง 6 แต่ multi-document transaction และ change stream (โมดูล 7) ใช้ได้เฉพาะบน replica set และ replica set หนึ่งโหนดก็เพียงพอ นี่คือสิ่งที่โค้ด client เดียวกันทำบน container แบบ standalone (output จริง):
txn: MongoServerError: This MongoDB deployment does not support retryable writes. Please add retryWrites=false to your connection string.watch: MongoServerError: The $changeStream stage is only supported on replica sets or mongos (code 40573)บทนี้จึงให้ ทั้งสองรูปแบบ ใช้ standalone ไปก่อน เมื่อถึงโมดูล 7 ให้สลับเป็นแบบ replica set (ใช้ได้กับทุกโมดูลก่อนหน้าด้วย ถ้าอยากใช้ตั้งแต่ต้นก็ได้)
Standalone (docker run / compose 1) | replica set หนึ่งโหนด (compose 2) | |
|---|---|---|
| โมดูล 1-6 (CRUD, index, aggregation, modeling) | ได้ | ได้ |
| Transaction, change stream (โมดูล 7) | ไม่ได้ | ได้ |
| Root user / auth | ได้ (MONGO_INITDB_ROOT_*) | ไม่ได้ — mongod --replSet rs0 --auth จบการทำงานด้วย BadValue: security.keyFile is required when authorization is enabled with replica sets (ดูบท security) |
| Connection string | mongodb://root:secret@localhost:27017 | mongodb://localhost:27017/?replicaSet=rs0 |
docker run บรรทัดเดียว (standalone)
หัวข้อที่มีชื่อว่า “docker run บรรทัดเดียว (standalone)”คำสั่งนี้ดึง image ทางการ เริ่ม container ชื่อ mongo-dev เปิดเผยพอร์ต MongoDB เริ่มต้น 27017 เก็บข้อมูลไว้ใน named volume เพื่อให้ข้อมูลอยู่รอดข้าม restart และตั้ง username และ password ของ root:
docker run -d \ --name mongo-dev \ -p 27017:27017 \ -v mongo-data:/data/db \ -e MONGO_INITDB_ROOT_USERNAME=root \ -e MONGO_INITDB_ROOT_PASSWORD=secret \ mongo:8มีบางอย่างในคำสั่งนั้นที่ควรทำความเข้าใจ:
-dรัน container ในเบื้องหลัง (detached) จะได้ไม่ยึด terminal ของคุณไว้-p 27017:27017map พอร์ต27017ของ container เข้ากับพอร์ตเดียวกันบน host ของคุณ นั่นคือที่ที่ MongoDB รับฟังโดยค่าเริ่มต้น-v mongo-data:/data/dbmount named volume ที่/data/dbซึ่งเป็นไดเรกทอรีที่ MongoDB เขียนไฟล์ลงไป volume อยู่นอก container ดังนั้นการลบและสร้าง container ใหม่จึงไม่ทำให้ข้อมูลของคุณหาย-e MONGO_INITDB_ROOT_USERNAME/-e MONGO_INITDB_ROOT_PASSWORDสร้าง user ระดับ administrator ในครั้งแรกที่ container เริ่มต้นไดเรกทอรีข้อมูลที่ว่างเปล่า (ระบุไว้ในหน้า image, hub.docker.com/_/mongo)
ตรวจว่ากำลังทำงานอยู่
หัวข้อที่มีชื่อว่า “ตรวจว่ากำลังทำงานอยู่”docker ps --format 'table {{.Image}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}'IMAGE STATUS PORTS NAMESmongo:8 Up 8 seconds 0.0.0.0:27017->27017/tcp, [::]:27017->27017/tcp mongodb-b1-devclient mongosh มาพร้อมกับ image คุณจึงรันได้โดยไม่ต้องติดตั้งอะไร ping ผ่าน shell ของ container เอง:
docker exec -it mongo-dev mongosh "mongodb://root:secret@localhost:27017" --eval 'db.runCommand({ ping: 1 })'{ ok: 1 }mongosh บน host ของคุณ (ถ้าติดตั้งไว้) รับ string เดียวกัน ชี้ไปที่พอร์ตที่เปิดออกมา: mongosh "mongodb://root:secret@localhost:27017" การเชื่อมต่อแบบ interactive ที่สำเร็จจะพาคุณไปที่ prompt test>
”เปิด auth” แปลว่าอะไรจริง ๆ
หัวข้อที่มีชื่อว่า “”เปิด auth” แปลว่าอะไรจริง ๆ”ตัวแปร root เปิดการยืนยันตัวตน เราพิสูจน์พฤติกรรมจริงกับ container นี้ โดยไม่ใส่ credentials ใน connection string:
> db.runCommand({ ping: 1 }) # ping needs no login{ ok: 1 }> db.members.find().toArray()MongoServerError: Command find requires authentication (code 13, codeName Unauthorized)ถ้าไม่มีตัวแปร MONGO_INITDB_ROOT_* สองตัว (และไม่มี --auth) image เดียวกันจะเริ่มด้วย ไม่มีการยืนยันตัวตนเลย และ bind ทุก interface: getCmdLineOpts รายงาน net: { bindIp: '*' } และ insertOne/find ที่ไม่ยืนยันตัวตนก็ใช้ได้เลย สะดวกบน laptop แต่อันตรายบนเครื่องใดก็ตามที่พอร์ต 27017 เข้าถึงได้จากภายนอก
ไฟล์ docker-compose (standalone)
หัวข้อที่มีชื่อว่า “ไฟล์ docker-compose (standalone)”สำหรับอะไรก็ตามที่เกินกว่าการทดสอบเร็ว ๆ, docker-compose.yml ดีกว่า: การตั้งค่าอยู่ในไฟล์ที่คุณ commit ได้ และคำสั่งเดียวก็ยกทุกอย่างขึ้นมา เวอร์ชันนี้รัน MongoDB และเพิ่ม mongo-express web UI น้ำหนักเบาสำหรับเรียกดูข้อมูล มีสองการเปลี่ยนแปลงจากการ copy-paste ตรง ๆ ซึ่งพิสูจน์แล้วทั้งคู่: image ของ UI ถูก pin (latest คือสิ่งที่บทนี้บอกให้หลีกเลี่ยง) และไม่มี container_name ดังนั้น docker compose exec mongo ... จึงหา service ด้วยชื่อ service
services: mongo: image: mongo:8 restart: unless-stopped ports: - "27017:27017" environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: secret volumes: - mongo-data:/data/db
mongo-express: image: mongo-express:1.0.2-20-alpine3.19 restart: unless-stopped ports: - "8081:8081" environment: ME_CONFIG_MONGODB_URL: "mongodb://root:secret@mongo:27017/" depends_on: - mongo
volumes: mongo-data:docker compose up -ddocker compose psdocker compose exec mongo mongosh "mongodb://root:secret@localhost:27017" --eval 'db.runCommand({ ping: 1 })'NAME IMAGE STATUS PORTSmongodb-b1-mongo-1 mongo:8 Up 15 seconds 0.0.0.0:27017->27017/tcp, [::]:27017->27017/tcpmongodb-b1-mongo-express-1 mongo-express:1.0.2-20-alpine3.19 Up 15 seconds 0.0.0.0:8081->8081/tcp, [::]:8081->8081/tcp{ ok: 1 }curl localhost:8081/ ได้ 401 และ curl -u admin:pass localhost:8081/ ได้ 200: mongo-express ป้องกันตัวเองด้วย basic auth ที่ค่าเริ่มต้นคือ admin:pass (log ของมันเองพิมพ์ basicAuth credentials are "admin:pass", it is recommended you change this) ตั้ง ME_CONFIG_BASICAUTH_USERNAME / ME_CONFIG_BASICAUTH_PASSWORD ก่อนเปิดพอร์ตให้ใคร หรือลบ service mongo-express ทิ้ง — มันเป็น optional
replica set หนึ่งโหนด (สำหรับ transaction และ change stream)
หัวข้อที่มีชื่อว่า “replica set หนึ่งโหนด (สำหรับ transaction และ change stream)”replica set ต้องการสองอย่างที่ standalone ไม่ต้องการ: mongod ที่เริ่มด้วย --replSet <name> และ rs.initiate(...) ครั้งเดียว เวอร์ชัน docker run ทีละขั้น:
docker run -d --name mongo-rs -p 27017:27017 mongo:8 mongod --replSet rs0 --bind_ip_allsleep 5docker exec mongo-rs mongosh --quiet --eval 'rs.initiate({ _id: "rs0", members: [{ _id: 0, host: "localhost:27017" }] })'ก่อน rs.initiate โหนดปฏิเสธการเขียน — เราบันทึกไว้:
> db.members.insertOne({ a: 1 })NotWritablePrimary not primary> db.hello().isWritablePrimaryfalseหลังจากนั้น db.hello() รายงาน setName: 'rs0' และ isWritablePrimary: true และคำตอบของ ping จะมี cluster time ($clusterTime, operationTime) ที่ standalone ไม่ส่งมา:
{ setName: 'rs0', isWritablePrimary: true, me: 'localhost:27017' }{ ok: 1, '$clusterTime': { clusterTime: Timestamp({ t: 1790747453, i: 18 }), ... }, operationTime: Timestamp({ t: 1790747453, i: 18 }) }สำหรับไฟล์ที่ commit ได้ ให้ healthcheck เป็นคน initiate เพื่อให้ docker compose up --wait คืนค่าก็ต่อเมื่อ replica set พร้อม:
services: mongo: image: mongo:8 command: ["mongod", "--replSet", "rs0", "--bind_ip_all"] ports: - "27017:27017" volumes: - mongo-rs-data:/data/db healthcheck: test: > mongosh --quiet --eval "try { rs.status().ok } catch (e) { rs.initiate({ _id: 'rs0', members: [{ _id: 0, host: 'localhost:27017' }] }).ok }" interval: 5s timeout: 10s retries: 10 start_period: 5s
volumes: mongo-rs-data:docker compose -f docker-compose.replset.yml up -d --wait พิมพ์ Container ... Healthy และ ps แสดง Up 5 seconds (healthy) host ของสมาชิกคือ localhost:27017 ซึ่งเข้าถึงได้จากเครื่องของคุณเพราะพอร์ตที่เปิดออกมาก็คือ 27017 เช่นกัน จาก host สคริปต์ Node นี้ (รันกับ container นั้น) ทำ transaction สำเร็จและเปิด change stream ได้ — ทั้งสองอย่างเป็นไปไม่ได้บน standalone:
// app/connect-replset.mjs @skip-verify needs the lesson's own replica-set container on host port 27017import { MongoClient } from "mongodb";
const client = new MongoClient("mongodb://localhost:27017/?replicaSet=rs0");await client.connect();console.log(Object.keys(await client.db("app").command({ ping: 1 }))); // [ 'ok', '$clusterTime', 'operationTime' ]
const session = client.startSession();await session.withTransaction(async () => { await client.db("app").collection("t").insertOne({ x: 1 }, { session });});console.log("txn ok");await client.close();ชิ้นส่วนทั้งหมดทำงานร่วมกันอย่างไร
หัวข้อที่มีชื่อว่า “ชิ้นส่วนทั้งหมดทำงานร่วมกันอย่างไร”container รัน MongoDB และรับฟังที่พอร์ต 27017 พอร์ตที่เปิดออกมาส่งต่อ localhost:27017 เข้าไปใน container, named volume เก็บข้อมูลให้ปลอดภัยนอก container และทุกอย่างที่พูดโปรโตคอลของ MongoDB — mongosh, Compass หรือแอปพลิเคชันของคุณ — เชื่อมต่อผ่านพอร์ตนั้น
flowchart LR App["Your app / mongosh / Compass"] -->|"mongodb://localhost:27017"| Port["Host port 27017"] Port --> Container["mongo container"] Container -->|"writes to /data/db"| Volume["named volume: mongo-data"]
การตรวจด้วย mongosh สองอย่างด้านล่างรันกับ harness ของบทเรียน (replica set หนึ่งโหนด) จึงพิมพ์สิ่งที่คุณจะเห็นบนแบบ replica set:
const h = db.hello();console.log({ setName: h.setName, isWritablePrimary: h.isWritablePrimary, version: db.version() });assert.strictEqual(h.isWritablePrimary, true);assert.strictEqual(db.runCommand({ ping: 1 }).ok, 1);{ setName: 'rs0', isWritablePrimary: true, version: '8.3.11' }เคล็ดลับและข้อควรระวัง
หัวข้อที่มีชื่อว่า “เคล็ดลับและข้อควรระวัง”- ข้อมูลถาวรอยู่ใน volume ไม่ใช่ container ตราบใดที่คุณเก็บ volume
mongo-dataไว้ คุณหยุด ลบ และสร้าง container ใหม่ได้โดยไม่เสียอะไรdocker volume rm mongo-dataคือสิ่งที่ลบข้อมูลจริง - root user ถูกสร้างเฉพาะตอน init ครั้งแรก เราพิสูจน์แล้ว: หลัง
docker rm -fเราสร้าง container ใหม่บน volume เดิมด้วยMONGO_INITDB_ROOT_PASSWORD=otherการ login ด้วยรหัส เก่าsecretยังได้ (authenticatedUsers: [ { user: 'root', db: 'admin' } ]) และรหัสใหม่ได้MongoServerError: Authentication failed.ตัวแปรถูกเมินเมื่อไดเรกทอรีข้อมูลไม่ว่าง - พอร์ต
27017คือค่าเริ่มต้นของ MongoDB ถ้ามีอย่างอื่นใช้อยู่ ให้เปลี่ยนฝั่ง host ของ mapping เช่น-p 27018:27017แล้วเชื่อมต่อ27018(ดู callout ของ replica set ด้านบน) - Pin major version เช่น
mongo:8แทนmongo:latestเพื่อให้การอัปเดต image ไม่ทำให้คุณเจอเวอร์ชัน server ที่ต่างไปโดยไม่รู้ตัวmongo:8resolve เป็น 8.3.11 เมื่อ 2026-09-30; 8.3 เป็น rapid-release minor ของ major 8; ส่วนmongo:8.0pin ชุด 8.0 แทน
อ่านโค้ดที่ AI สร้าง
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้าง”ลองขอผู้ช่วย AI ว่า “MongoDB replica set ใน Docker สำหรับ transaction” คุณมักได้ไฟล์ compose นี้ ที่ดูสมบูรณ์:
🐛 หา bug ในโค้ดนี้
# AI comment: "replica set enabled, so transactions and change streams will work"services:mongo: image: mongo:latest command: ["mongod", "--replSet", "rs0", "--bind_ip_all"] ports: - "27017:27017"เฉลย
--replSet rs0 เพียงบอก mongod ว่า มันจะเป็นสมาชิกของ set ไหน ไม่มีใครเรียก rs.initiate เลย โหนดจึงไม่มี config และไม่มี primary ผลจริงบน mongo:8: db.members.insertOne({ a: 1 }) ล้มเหลวด้วย NotWritablePrimary not primary และ db.hello().isWritablePrimary เป็น false — ทุกการเขียนและทุก transaction ถูกปฏิเสธ ตรงข้ามกับคำสัญญาใน comment (และยัง pin latest) วิธีแก้คือการ initiate ด้วย healthcheck จากหัวข้อก่อนหน้า ซึ่งเรารันจนถึง Healthy แล้วตามด้วย transaction จาก host ที่ commit สำเร็จ:
services: mongo: image: mongo:8 command: ["mongod", "--replSet", "rs0", "--bind_ip_all"] ports: - "27017:27017" healthcheck: test: > mongosh --quiet --eval "try { rs.status().ok } catch (e) { rs.initiate({ _id: 'rs0', members: [{ _id: 0, host: 'localhost:27017' }] }).ok }" interval: 5s timeout: 10s retries: 10 start_period: 5sMental Model
หัวข้อที่มีชื่อว่า “Mental Model”container คือ process บวกไดเรกทอรีหนึ่ง: mongod ที่รันกับ /data/db ทุกอย่างที่เหลือ — การ map พอร์ต, volume, ตัวแปร env — คือสายไฟรอบสองสิ่งนั้น ตัวแปร env ถูกอ่าน ครั้งเดียว โดย entrypoint ของ image และเฉพาะเมื่อไดเรกทอรีว่าง หลังจากนั้นไดเรกทอรี (user และ config ของ replica set ในนั้น) คือความจริง แนวคิดเดียวนี้อธิบายเรื่องรหัส root ที่ไม่เปลี่ยน, “replica set ที่ไม่เคยถูก initiate” และเหตุผลที่การลบ volume คือปุ่ม reset จริง
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| รัน MongoDB ผ่าน Docker | ล้างและสร้างใหม่ได้เร็ว ไม่ทิ้งอะไรถาวรบนเครื่อง และสภาพแวดล้อมใกล้เคียง production | ต้องมี Docker engine ทำงานตลอด และ container เพิ่ม overhead เล็กน้อยเทียบกับรันบน host โดยตรง |
| ติดตั้ง MongoDB บนเครื่องโดยตรง | ไม่มีชั้น container คั่น เชื่อมต่อผ่าน localhost ตรง ๆ | รีเซ็ตหรือเปลี่ยนเวอร์ชันยากกว่า และเครื่องในทีมอาจได้เวอร์ชันหรือ config ไม่ตรงกัน |
| container แบบ standalone | ง่ายที่สุด รองรับ auth แบบ root user ได้ทันที | ไม่มี transaction ไม่มี change stream (พิสูจน์ด้านบน) |
| replica set หนึ่งโหนด | transaction และ change stream ใช้ได้ ตรงกับวิธีที่ production รัน | มีขั้น rs.initiate เพิ่ม ไม่มี auth ถ้าไม่มี keyfile และกับดัก host สมาชิก/การ map พอร์ต |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “ใช้
mongo:latestเพราะใหม่สุด” —docker pullครั้งถัดไปเปลี่ยนเวอร์ชัน server ใต้เท้าคุณได้ ให้ pinmongo:8(หรือ8.0) - “ลบ container แล้วข้อมูลหาย” — ถ้ามี named volume ไม่หาย; ในทางกลับกันถ้าไม่มี volume เลยข้อมูลหายไปพร้อม container
docker rmกับdocker volume rmเป็นคนละ operation - “
MONGO_INITDB_ROOT_PASSWORDรีเซ็ตรหัสผ่านได้” — ไม่ได้ มันมีผลกับไดเรกทอรีข้อมูลที่ว่างเท่านั้น (พิสูจน์ด้านบน) - “
--replSetทำให้เป็น replica set” — ต้องมีrs.initiateด้วย มิฉะนั้นการเขียนล้มเหลวด้วยNotWritablePrimary - “เป็นเครื่อง dev ที่ไม่มีใครแตะ ไม่ต้องใส่รหัสผ่าน” — ถ้าไม่มีตัวแปร root image จะ bind
*และไม่ต้อง login; ทุกอย่างที่เข้าถึงพอร์ต 27017 ได้ก็อ่านและเขียนได้
บทนี้ไม่ใช้ Python/Go/Rust: เป็น Docker และ shell ล้วน