Bài thực hành dành cho người mới: tự viết website giới thiệu Docker bằng Python Flask, đóng gói thành image và chạy trong container. Kết quả là website màu xanh có menu, mô hình các thành phần và năm thẻ kiến thức, mở được tại http://localhost:8081.
Toàn bộ lệnh trong bài chạy trong terminal Ubuntu WSL. Điều kiện bắt đầu là máy đã có Ubuntu trên WSL 2, có Internet và tài khoản có quyền sudo. Windows chỉ dùng để mở Ubuntu và xem website bằng trình duyệt. Thời gian dự kiến 60–90 phút.
Copy bằng nút sao chép của khối mã hoặc chọn đúng nội dung bên trong khối. Không copy qua Word hoặc ứng dụng có thể thay đổi dấu nháy và khoảng trắng. Với Python, giữ nguyên thụt đầu dòng.
Quy ước thư mục: ~ là thư mục cá nhân của tài khoản Ubuntu. ~/docker-flask là thư mục gốc dự án. templates là thư mục con, còn /app là thư mục bên trong container. Trước mỗi thao tác, đọc dòng “Nơi thực hiện”; dùng pwd để kiểm tra. Nếu còn ở Nano, lưu và thoát trước khi nhập lệnh terminal. Nếu còn trong container, nhập exit để về Ubuntu.
Mục lục
1. Kiểm tra Ubuntu WSL
Mở Ubuntu từ Start. Chạy từng lệnh sau để xem hệ điều hành, kernel và tài khoản hiện tại:
Nơi thực hiện: terminal Ubuntu WSL, bất kỳ thư mục nào. Các lệnh kiểm tra hệ thống, cài phần mềm và quản lý dịch vụ không phụ thuộc thư mục dự án.
cat /etc/os-release
uname -r
whoami
pwd
Bạn cần thấy hệ điều hành Ubuntu. Kernel WSL 2 thường có chuỗi microsoft-standard-WSL2. pwd cho biết thư mục hiện tại. Nếu chưa có Ubuntu WSL 2, cần chuẩn bị môi trường đó trước khi bắt đầu lab.
Những khái niệm cần phân biệt: Flask nhận yêu cầu HTTP và trả trang web; HTML/CSS tạo giao diện; Dockerfile hướng dẫn đóng gói; image chứa ứng dụng cùng môi trường; container là môi trường được tạo từ image để thực thi chương trình.
2. Cài đặt và khởi động Docker
2.1. Kiểm tra Docker hiện có
Nơi thực hiện: terminal Ubuntu WSL, bất kỳ thư mục nào. Các lệnh kiểm tra hệ thống, cài phần mềm và quản lý dịch vụ không phụ thuộc thư mục dự án.
sudo docker version
Nếu kết quả có cả Client và Server, Docker đã sẵn sàng: bỏ qua cài đặt và chuyển đến phần 3. Nếu đã dùng Docker Desktop tích hợp WSL và Docker đang hoạt động, bạn vẫn thực hành toàn bộ lệnh trong Ubuntu. Không cài thêm một Engine để thay đổi môi trường đang chạy tốt.
2.2. Cập nhật danh sách phần mềm
Nơi thực hiện: terminal Ubuntu WSL, bất kỳ thư mục nào. Các lệnh kiểm tra hệ thống, cài phần mềm và quản lý dịch vụ không phụ thuộc thư mục dự án.
sudo apt update
apt quản lý gói phần mềm Ubuntu; update cập nhật danh sách gói, chưa nâng cấp toàn bộ hệ thống. sudo chạy với quyền quản trị Linux. Khi nhập mật khẩu Ubuntu, terminal không hiện ký tự hoặc dấu sao.
2.3. Cài Docker và công cụ thực hành
Nơi thực hiện: terminal Ubuntu WSL, bất kỳ thư mục nào. Các lệnh kiểm tra hệ thống, cài phần mềm và quản lý dịch vụ không phụ thuộc thư mục dự án.
sudo apt install -y docker.io curl nano
Bước này dành cho Ubuntu chưa có Docker. docker.io là Docker do kho Ubuntu phân phối; curl dùng kiểm tra HTTP; nano dùng soạn file. -y tự đồng ý xác nhận cài đặt. Cách này không cần thêm khóa hoặc kho phần mềm Docker. Không cài docker.io chồng lên bộ docker-ce đã có từ kho Docker.
2.4. Khởi động Docker
Nơi thực hiện: terminal Ubuntu WSL, bất kỳ thư mục nào. Các lệnh kiểm tra hệ thống, cài phần mềm và quản lý dịch vụ không phụ thuộc thư mục dự án.
sudo service docker start
sudo docker version
Đạt yêu cầu khi thấy cả Client (công cụ dòng lệnh) và Server (Docker Engine). Nếu service báo lỗi nhưng Ubuntu có systemd, có thể dùng sudo systemctl start docker rồi kiểm tra lại. Không tiếp tục khi Engine chưa kết nối được.
2.5. Chạy container kiểm tra
Nơi thực hiện: terminal Ubuntu WSL, bất kỳ thư mục nào. Các lệnh kiểm tra hệ thống, cài phần mềm và quản lý dịch vụ không phụ thuộc thư mục dự án.
sudo docker run --rm hello-world
Docker tải image nếu chưa có, tạo container và chạy chương trình. Thấy Hello from Docker! là thành công. --rm tự xóa container thử sau khi chương trình kết thúc. Lab giữ sudo trước lệnh Docker để tránh lỗi quyền truy cập.
3. Tạo thư mục dự án
Nơi bắt đầu: terminal Ubuntu WSL, thư mục cá nhân ~. Sau các lệnh dưới, bạn đứng tại ~/docker-flask, còn templates là thư mục con.
cd ~
mkdir -p ~/docker-flask/templates
cd ~/docker-flask
pwd
mkdir tạo thư mục; -p tạo cả thư mục cha khi cần; ~ là thư mục cá nhân Linux. Kết quả pwd thường là /home/tenban/docker-flask. Mỗi lần mở terminal mới, quay lại bằng cd ~/docker-flask.
Dự án gồm app.py, requirements.txt, Dockerfile, .dockerignore và templates/index.html. Trong Nano, dán nội dung rồi nhấn Ctrl+O, Enter để lưu và Ctrl+X để thoát.
4. Viết chương trình Flask
4.1. Tạo file app.py
Nơi thực hiện: terminal Ubuntu WSL, thư mục ~/docker-flask (ví dụ /home/tenban/docker-flask).
cd ~/docker-flask
pwd
nano app.py
Dán nguyên đoạn Python sau. Hai dòng bên trong hàm và điều kiện cuối có bốn dấu cách phía trước:
Nội dung file: app.py trong ~/docker-flask. Dán vào Nano đang mở; đây là Python, không phải lệnh terminal.
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/")
def home():
return render_template("index.html")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
Flask(__name__) tạo ứng dụng. @app.route("/") đăng ký đường dẫn trang chủ. Khi có yêu cầu, home() gọi render_template để đọc templates/index.html. app.run khởi động server khi chạy trực tiếp file. 0.0.0.0 cho phép nhận kết nối qua mạng container; 8000 là cổng bên trong.
4.2. Cách tạo file khi thao tác dán làm mất định dạng
Nếu đã thoát Nano, bạn có thể copy nguyên khối dưới đây vào terminal Ubuntu. Lệnh ghi đè app.py và giữ nguyên các dòng. EOF cuối phải đứng riêng, không có khoảng trắng phía trước:
Nơi thực hiện: terminal Ubuntu WSL, thư mục ~/docker-flask (ví dụ /home/tenban/docker-flask).
cd ~/docker-flask
pwd
cat > app.py <<'EOF'
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/")
def home():
return render_template("index.html")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
EOF
Chỉ chọn một trong hai cách tạo app.py. Kiểm tra nội dung bằng:
Nơi thực hiện: terminal Ubuntu WSL, thư mục ~/docker-flask (ví dụ /home/tenban/docker-flask).
cd ~/docker-flask
pwd
cat app.py
4.3. Khai báo thư viện
Nơi thực hiện: terminal Ubuntu WSL, thư mục ~/docker-flask (ví dụ /home/tenban/docker-flask).
cd ~/docker-flask
pwd
nano requirements.txt
Nội dung file: ~/docker-flask/requirements.txt. Dán dòng dưới vào Nano, sau đó lưu và thoát.
Flask==3.1.3
Lưu bằng Ctrl+O, Enter rồi thoát bằng Ctrl+X. Dòng Flask==3.1.3 yêu cầu cài đúng phiên bản Flask 3.1.3. Dấu == cố định phiên bản Flask cho bài lab. Docker sẽ cài Flask trong image qua requirements.txt, nên không cần cài Flask trên Ubuntu để chạy lab. Nếu đã build image trước khi sửa file, cần build lại image và tạo lại container theo mục 10 để áp dụng.
5. Tạo giao diện website
Nơi thực hiện: terminal Ubuntu WSL, thư mục ~/docker-flask (ví dụ /home/tenban/docker-flask). Mở templates/index.html từ thư mục gốc dự án; không cần cd vào templates.
cd ~/docker-flask
pwd
nano templates/index.html
Dán toàn bộ khối sau vào cùng một file, sau đó lưu và thoát:
Nội dung file: ~/docker-flask/templates/index.html. Dán toàn bộ vào Nano đang mở rồi lưu; không chạy HTML trong terminal.
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Docker Learning Lab</title>
<style>
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body { margin: 0; font-family: Arial, sans-serif;
color: #122144; background: #f2f7ff; }
a { color: inherit; text-decoration: none; }
header { background: white; padding: 22px 5%;
display: flex; justify-content: space-between;
align-items: center; gap: 20px; }
.logo { font-size: 30px; font-weight: bold; color: #075bff; }
nav { display: flex; gap: 24px; }
main { max-width: 1200px; margin: auto; padding: 55px 5%; }
.hero { display: grid; grid-template-columns: 1fr 1fr;
gap: 36px; align-items: center; }
.label { color: #075bff; letter-spacing: 3px; font-size: 13px; }
h1 { color: #075bff; font-size: clamp(60px, 8vw, 96px);
margin: 15px 0; letter-spacing: -3px; }
h2 { font-size: 30px; }
p { color: #536582; line-height: 1.7; }
.button { display: inline-block; background: #075bff;
color: white; padding: 16px 22px; border-radius: 12px; }
.diagram { background: #deedff; padding: 24px;
border-radius: 22px; box-shadow: 0 15px 45px #123c7315; }
.row { display: grid; grid-template-columns: 1fr 1fr;
gap: 12px; }
.box, .card { background: white; border: 1px solid #dce7f8;
padding: 20px; border-radius: 14px; }
.box p { font-size: 13px; margin-bottom: 0; }
.container { text-align: center; background: #0864df;
color: white; padding: 36px 12px; margin: 18px 0;
border-radius: 12px; border: 5px solid #348eff; }
.container p { color: white; }
.engine { text-align: center; background: #163760;
color: white; padding: 16px; border-radius: 10px;
margin-top: 16px; }
.cards { display: grid; grid-template-columns: repeat(5, 1fr);
gap: 14px; margin-top: 28px; }
.card h3 { color: #075bff; font-size: 17px; }
.card p { font-size: 14px; }
section { scroll-margin-top: 20px; }
#thanh-phan { margin-top: 55px; }
footer { text-align: center; padding: 28px; color: #536582; }
@media (max-width: 900px) {
.hero { grid-template-columns: 1fr; }
.cards { grid-template-columns: repeat(2, 1fr); }
nav { flex-wrap: wrap; gap: 12px; }
}
@media (max-width: 540px) {
.cards, .row { grid-template-columns: 1fr; }
header { flex-direction: column; }
}
</style>
</head>
<body>
<header>
<a class="logo" href="#">Docker</a>
<nav aria-label="Menu chính">
<a href="#tong-quan">Tổng quan</a>
<a href="#mo-hinh">Mô hình</a>
<a href="#thanh-phan">Thành phần</a>
</nav>
</header>
<main>
<section class="hero" id="tong-quan">
<div>
<span class="label">CONTAINER PLATFORM</span>
<h1>Docker</h1>
<h2>Đóng gói và chạy nhất quán</h2>
<p>Đóng gói ứng dụng cùng môi trường cần thiết vào image.
Tạo container từ image để chạy trên hệ thống phù hợp.</p>
<a class="button" href="#thanh-phan">Bắt đầu học Docker</a>
</div>
<div class="diagram" id="mo-hinh">
<div class="row">
<div class="box"><strong>Docker Registry</strong>
<p>Lưu trữ và phân phối image.</p></div>
<div class="box"><strong>Docker CLI</strong>
<p>Gửi lệnh build, run và kiểm tra container.</p></div>
</div>
<div class="container"><h2>Docker Container</h2>
<p>Website Flask của bạn đang chạy tại đây</p></div>
<div class="row">
<div class="box"><strong>Docker Image</strong>
<p>Python, Flask và source code website.</p></div>
<div class="box"><strong>Volume và Network</strong>
<p>Lưu dữ liệu và cung cấp kết nối mạng.</p></div>
</div>
<div class="engine">Docker Engine quản lý image và container</div>
</div>
</section>
<section id="thanh-phan">
<h2>Các thành phần cốt lõi</h2>
<div class="cards">
{% for name, description in [
('Image', 'Gói ứng dụng và môi trường để tạo container.'),
('Container', 'Môi trường thực thi được tạo từ image.'),
('Volume', 'Lưu dữ liệu độc lập với vòng đời container.'),
('Network', 'Kết nối container với các hệ thống khác.'),
('Registry', 'Kho lưu trữ và phân phối image.')
] %}
<article class="card"><h3>Docker {{ name }}</h3>
<p>{{ description }}</p></article>
{% endfor %}
</div>
</section>
</main>
<footer>Phiên bản 1.0 | Flask Docker Lab | @techdata.ai</footer>
</body>
</html>
HTML tổ chức trang; CSS trong style tạo màu sắc và bố cục. Các điều kiện @media điều chỉnh cho màn hình nhỏ. Vòng lặp {% for %} là Jinja: Flask tạo một thẻ cho mỗi tên và mô tả. {{ name }} và {{ description }} được thay bằng dữ liệu. Vì có Jinja, xem trang qua Flask thay vì mở trực tiếp file HTML. Giao diện minh họa bố cục ảnh mẫu bằng HTML/CSS; không sử dụng hình 3D bên ngoài.
6. Viết Dockerfile
Nơi thực hiện: terminal Ubuntu WSL, thư mục ~/docker-flask (ví dụ /home/tenban/docker-flask).
cd ~/docker-flask
pwd
nano Dockerfile
Lệnh này là gì? cd chuyển thư mục làm việc sang ~/docker-flask; pwd in vị trí hiện tại; nano Dockerfile mở trình soạn thảo để tạo hoặc sửa file Dockerfile. Nếu file chưa tồn tại, Nano tạo file khi bạn lưu.
Tại sao cần đứng ở đây? Dockerfile được đặt cùng cấp với app.py, requirements.txt và thư mục templates để các đường dẫn COPY khớp với build context. nano chỉ mở trình soạn thảo, chưa build image hay chạy ứng dụng. Sau khi dán nội dung, lưu bằng Ctrl+O, Enter và thoát bằng Ctrl+X.
Nội dung file: ~/docker-flask/Dockerfile. Dán vào Nano đang mở. WORKDIR /app chỉ thư mục bên trong image và container, không yêu cầu bạn chuyển Ubuntu vào /app.
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
COPY templates/ ./templates/
EXPOSE 8000
CMD ["python", "-u", "app.py"]
FROM python:3.12-slim là gì? Chọn image nền đã có Python 3.12 và phần môi trường hệ thống cần thiết. slim là biến thể gọn của image Python. Nhờ image nền, bạn không phải tự viết các bước cài Python từ đầu; image này không chứa nguyên một máy Ubuntu WSL.
WORKDIR /app làm gì? Đặt /app làm thư mục làm việc cho các lệnh sau trong image và khi container chạy. Docker tạo thư mục nếu chưa có. Đây là đường dẫn bên trong image, không phải yêu cầu đổi thư mục Ubuntu trên máy bạn.
COPY requirements.txt . vì sao có dấu chấm? Bên trái là file trong build context; dấu chấm bên phải là thư mục làm việc hiện tại /app. Kết quả là /app/requirements.txt. COPY sao chép file, không chạy nội dung file.
RUN pip install --no-cache-dir -r requirements.txt là gì? RUN thực thi lệnh khi build. pip là công cụ cài thư viện Python; install yêu cầu cài; -r đọc danh sách từ requirements.txt. --no-cache-dir không giữ bộ nhớ đệm tải thư viện của pip trong image. Lệnh cài Flask và các thư viện phụ thuộc để app.py có thể import chúng.
Vì sao copy requirements trước source code? Docker có thể tái sử dụng kết quả cài thư viện nếu requirements và các bước trước không thay đổi. Khi chỉ sửa HTML hoặc app.py, bước pip thường được dùng lại từ cache, giúp build nhanh hơn. --no-cache-dir của pip không có nghĩa là tắt build cache của Docker.
COPY app.py . và COPY templates/ ./templates/ làm gì? Đưa chương trình vào /app/app.py và giao diện vào /app/templates/index.html. Flask cần đúng thư mục templates để render_template tìm thấy trang. Chỉ cài Flask mà không copy source thì container chưa có website của bạn.
EXPOSE 8000 có mở cổng không? Đây là thông tin mô tả cổng mà ứng dụng dự kiến dùng. Nó không làm Flask tự lắng nghe và không publish cổng ra máy. Flask lắng nghe nhờ app.run; Docker đưa cổng ra ngoài bằng -p ở mục 8.
CMD ["python", "-u", "app.py"] vì sao viết dạng danh sách? Đây là dạng exec: mỗi phần tử là một thành phần của lệnh. python chạy trình thông dịch; -u giúp xuất log không bị giữ trong bộ đệm; app.py là file cần chạy trong /app. CMD ghi lệnh mặc định để dùng lúc khởi động container; nó không chạy Flask trong lúc build. Có thể ghi đè lệnh mặc định khi dùng docker run.
Tên file là Dockerfile, viết hoa D, không có đuôi .txt. FROM chọn image có Python. WORKDIR đặt /app làm thư mục làm việc. COPY đưa file từ dự án vào image. RUN cài thư viện trong lúc build. CMD ghi lệnh mặc định lúc container khởi động. -u giúp log Python xuất ra ngay. EXPOSE 8000 mô tả cổng ứng dụng, chưa publish cổng ra ngoài.
Tạo .dockerignore để bỏ qua các file không cần thiết:
Nơi thực hiện: terminal Ubuntu WSL, thư mục ~/docker-flask (ví dụ /home/tenban/docker-flask).
cd ~/docker-flask
pwd
nano .dockerignore
Lệnh này là gì và tại sao cần? nano .dockerignore tạo file cấu hình loại trừ trong thư mục gốc dự án. Docker đọc file này khi chuẩn bị build context để tránh gửi các file không cần thiết vào quá trình build. Dấu chấm đầu tên file làm nó trở thành file ẩn trong Linux; dùng ls -la để thấy.
Nội dung file: ~/docker-flask/.dockerignore. Dán vào Nano, sau đó lưu và thoát.
.venv
__pycache__
.git
*.pyc
Các dòng này là mẫu loại trừ, không phải lệnh. .venv là môi trường Python cục bộ; __pycache__ và *.pyc là dữ liệu bytecode Python; .git là lịch sử Git. Image đã dùng môi trường Python riêng nên không cần mang các file này vào. Quy tắc chỉ loại chúng khỏi build context, không xóa chúng khỏi Ubuntu. Với COPY cụ thể trong lab, đây cũng là chuẩn bị cho khi dự án có thêm file.
Kiểm tra đủ file trước khi build:
Nơi thực hiện: terminal Ubuntu WSL, thư mục ~/docker-flask (ví dụ /home/tenban/docker-flask).
cd ~/docker-flask
pwd
ls -la
ls -l templates
cat Dockerfile
ls -la là gì? ls liệt kê nội dung; -l hiển thị chi tiết; -a bao gồm file ẩn như .dockerignore. ls -l templates kiểm tra riêng thư mục giao diện. cat Dockerfile đọc nội dung ra terminal mà không thực thi.
Tại sao kiểm tra trước khi build? Bước này phát hiện thiếu file, sai chữ hoa/thường hoặc đặt index.html sai chỗ. Nếu không có file được COPY, build sẽ thất bại; nếu đặt giao diện sai đường dẫn, Flask có thể báo TemplateNotFound khi nhận yêu cầu.
7. Build image
Nơi thực hiện: terminal Ubuntu WSL, thư mục ~/docker-flask (ví dụ /home/tenban/docker-flask).
cd ~/docker-flask
pwd
sudo docker build -t docker-flask:1.0 .
docker build là gì? Yêu cầu Docker tạo image theo Dockerfile. -t là viết tắt của tag, gán tên và nhãn phiên bản. docker-flask:1.0 hoặc docker-flask:2.0 gồm tên docker-flask và tag sau dấu hai chấm. Tag là nhãn do bạn đặt; Docker không tự hiểu rằng 2.0 là bản nâng cấp nếu bạn chưa build và chạy đúng image.
Dấu chấm cuối vì sao quan trọng? Nó chọn thư mục hiện tại làm build context. Dockerfile dùng COPY để lấy file từ context này, nên cd và pwd giúp chắc chắn bạn đang chọn đúng dự án. Với lab, Docker mặc định tìm Dockerfile tại gốc context.
Khi build xảy ra điều gì? Docker chuẩn bị image nền, cài Flask và copy source theo từng bước. Cache có thể giúp bỏ qua bước không thay đổi. Build chỉ tạo image; không tự thay thế container đang chạy. Chỉ chuyển sang bước chạy hoặc thay container khi build kết thúc không lỗi.
-t đặt tên image và tag. docker-flask là tên, 1.0 là tag. Dấu chấm cuối chỉ thư mục hiện tại làm build context, tức tập file Docker có thể dùng khi build. Lần đầu cần tải Python và Flask. Chỉ tiếp tục khi build kết thúc thành công.
Nơi thực hiện: terminal Ubuntu WSL, thư mục ~/docker-flask (ví dụ /home/tenban/docker-flask). Các lệnh docker ps, logs, exec, run, stop, start và rm trong lab này có thể chạy ở bất kỳ thư mục Ubuntu nào vì chúng gọi Docker Engine; giữ tại thư mục dự án để dễ theo dõi.
sudo docker image ls docker-flask
docker image ls là gì? Liệt kê image đang có trong Docker Engine; đối số docker-flask lọc theo tên image. REPOSITORY là tên, TAG là nhãn phiên bản, IMAGE ID là định danh image.
Tại sao cần lệnh này? Nó xác nhận image vừa build đã tồn tại và có đúng tag để dùng cho docker run. Có image không đồng nghĩa đã có container, nên website có thể vẫn chưa truy cập được ở bước này.
Bạn cần thấy image docker-flask với tag 1.0. Lúc này đã có gói ứng dụng nhưng website chưa chạy.
8. Chạy container và mở website
Nơi thực hiện: terminal Ubuntu WSL, thư mục ~/docker-flask (ví dụ /home/tenban/docker-flask). Các lệnh docker ps, logs, exec, run, stop, start và rm trong lab này có thể chạy ở bất kỳ thư mục Ubuntu nào vì chúng gọi Docker Engine; giữ tại thư mục dự án để dễ theo dõi.
sudo docker run -d --name flask-web -p 127.0.0.1:8081:8000 docker-flask:1.0
docker run là gì? Tạo một container mới từ image và khởi động nó. Docker dùng image có sẵn; nếu tag không có cục bộ, Docker có thể thử tải từ registry. Vì vậy cần dùng đúng tên và tag đã build.
-d và --name có ý nghĩa gì? -d chạy tách khỏi terminal để bạn tiếp tục gõ lệnh. --name gán tên dễ nhớ như flask-web hoặc flask-web-8082 để dùng với logs, stop và exec. Một tên container phải duy nhất trong cùng Docker Engine, kể cả khi container đã dừng.
-p 127.0.0.1:8081:8000 đọc thế nào? Ba phần lần lượt là địa chỉ bind phía máy chạy Docker, cổng ngoài và cổng container. Trình duyệt dùng localhost:8081; Flask vẫn nghe 8000. Khi ví dụ đổi thành 8082:8000, chỉ địa chỉ truy cập bên ngoài đổi sang 8082. Bind 127.0.0.1 giới hạn cổng ở localhost phía Docker host.
Tại sao cần publish cổng? Ứng dụng có mạng riêng trong container; -p tạo điểm truy cập từ bên ngoài. EXPOSE trong Dockerfile chưa làm việc này. Với WSL có chuyển tiếp localhost phù hợp, trình duyệt Windows truy cập được qua localhost.
Vì sao container tiếp tục chạy? CMD chạy Flask ở foreground bên trong container, dù Docker được gọi với -d từ terminal. Tiến trình chính còn chạy thì container còn hoạt động; nếu Python lỗi hoặc kết thúc, container sẽ dừng.
-d chạy nền; --name đặt tên container; -p chuyển kết nối từ cổng 8081 trên localhost vào cổng 8000 trong container, giới hạn truy cập ở localhost. Docker thực thi CMD, tương đương python -u app.py, để Flask bắt đầu phục vụ.
Nơi thực hiện: terminal Ubuntu WSL, thư mục ~/docker-flask (ví dụ /home/tenban/docker-flask). Các lệnh docker ps, logs, exec, run, stop, start và rm trong lab này có thể chạy ở bất kỳ thư mục Ubuntu nào vì chúng gọi Docker Engine; giữ tại thư mục dự án để dễ theo dõi.
sudo docker ps
curl -I http://localhost:8081
docker ps cho biết gì? Liệt kê container đang chạy. Cột STATUS cần là Up; PORTS cho biết ánh xạ 8081 với 8000. Container không có trong ps có thể đã dừng; dùng ps -a để tìm.
curl -I kiểm tra gì? Gửi yêu cầu HEAD đến website để xem header và mã trạng thái. Nếu nhận HTTP 200, ứng dụng trả lời được từ Ubuntu. Tiếp theo mở trình duyệt Windows để kiểm tra nội dung và giao diện; curl không tự mở cửa sổ web.
Đạt yêu cầu khi container flask-web có trạng thái Up và curl trả HTTP 200. Mở Chrome hoặc Edge trên Windows tại http://localhost:8081. Bạn phải thấy tiêu đề Docker, năm thẻ kiến thức và Phiên bản 1.0 ở cuối trang. Menu và nút Bắt đầu học Docker cuộn đến phần tương ứng.
9. Kiểm tra hoạt động
Nơi thực hiện: terminal Ubuntu WSL, thư mục ~/docker-flask (ví dụ /home/tenban/docker-flask). Các lệnh docker ps, logs, exec, run, stop, start và rm trong lab này có thể chạy ở bất kỳ thư mục Ubuntu nào vì chúng gọi Docker Engine; giữ tại thư mục dự án để dễ theo dõi.
sudo docker logs flask-web
sudo docker logs -f flask-web
docker logs là gì? Đọc dữ liệu stdout và stderr mà ứng dụng trong container ghi ra. flask-web xác định container cần xem. -f nghĩa là follow, tiếp tục theo dõi các dòng log mới thay vì xem một lần rồi kết thúc.
Tại sao xem log? Log giúp biết Flask đã khởi động chưa, có lỗi Python hay không và yêu cầu HTTP nhận mã gì. GET / với 200 cho biết trang chủ trả thành công. Ctrl+C khi theo dõi logs chỉ ngắt lệnh xem log, không dừng container.
Refresh trình duyệt để thấy GET / với mã 200. Ctrl+C thoát theo dõi log; container vẫn chạy nền. favicon.ico trả 404 không có nghĩa trang chủ hỏng.
Vào shell bên trong container:
Nơi thực hiện: terminal Ubuntu WSL, thư mục ~/docker-flask (ví dụ /home/tenban/docker-flask). Các lệnh docker ps, logs, exec, run, stop, start và rm trong lab này có thể chạy ở bất kỳ thư mục Ubuntu nào vì chúng gọi Docker Engine; giữ tại thư mục dự án để dễ theo dõi. Sau lệnh này, bạn chuyển vào shell của container tại /app.
sudo docker exec -it flask-web sh
docker exec -it flask-web sh là gì? exec chạy thêm một lệnh trong container đang hoạt động; -i giữ luồng nhập mở; -t tạo terminal tương tác; flask-web chỉ container; sh là shell cần mở.
Tại sao dùng sh? Image gọn thường có sh, còn bash không phải lúc nào cũng có. Lệnh này giúp kiểm tra môi trường thực tế chứa source và Flask. Nó không tạo container mới và không tự cài công cụ.
Sau khi chạy bạn đang ở đâu? Trong container flask-web, mặc định /app theo WORKDIR. Các lệnh ls, pwd và python tiếp theo tác động môi trường container. Nhập exit để đóng shell này và quay về Ubuntu; Flask vẫn chạy vì shell exec không phải tiến trình chính.
Sau khi vào container, chạy:
Nơi thực hiện: shell bên trong container flask-web, thư mục /app. Chạy pwd để xác nhận. Dòng exit cuối quay lại Ubuntu WSL; các bước tiếp theo thực hiện ở Ubuntu.
pwd
ls -l
ls -l templates
python --version
python -m pip show Flask
exit
pwd và ls trong container cho biết gì? pwd in thư mục làm việc; ls -l liệt kê file chi tiết; ls -l templates kiểm tra giao diện. Bạn cần thấy /app, app.py và templates/index.html. Đây là kiểm tra bản source đã được đóng gói, không phải thư mục dự án trên Ubuntu.
python --version và python -m pip show Flask là gì? Lệnh đầu xem phiên bản Python trong image. Lệnh sau gọi module pip bằng đúng Python đó và xem thông tin Flask đã cài, gồm phiên bản và vị trí. Không nhầm kết quả này với Python hoặc Flask trên Ubuntu.
exit dùng để làm gì? Đóng shell đang mở trong container. Sau exit, bạn trở lại terminal Ubuntu ở thư mục trước khi chạy docker exec. Chạy cd ~/docker-flask và pwd nếu cần xác nhận trước bước sửa source.
pwd trong container trả /app. exit quay về Ubuntu WSL. /app/app.py là bản copy từ source code khi build, khác với ~/docker-flask/app.py trên Ubuntu.
10. Cập nhật website
Nơi thực hiện: terminal Ubuntu WSL, thư mục ~/docker-flask (ví dụ /home/tenban/docker-flask).
cd ~/docker-flask
pwd
nano templates/index.html
Vì sao sửa file này ở Ubuntu? cd chọn đúng gốc dự án, pwd xác nhận vị trí, nano mở file giao diện theo đường dẫn tương đối templates/index.html. Source ở đây là đầu vào cho lần build tiếp theo.
Tại sao refresh chưa thấy thay đổi? COPY tạo bản sao trong image lúc build, không tạo liên kết đồng bộ với file Ubuntu. Vì vậy cần lưu, thoát Nano, build tag mới và tạo container từ image đó.
Đổi Phiên bản 1.0 thành Phiên bản 2.0 ở footer rồi lưu. Refresh vẫn thấy bản cũ vì container dùng image cũ. Build mới trước:
Nơi thực hiện: terminal Ubuntu WSL, thư mục ~/docker-flask (ví dụ /home/tenban/docker-flask).
cd ~/docker-flask
pwd
sudo docker build -t docker-flask:2.0 .
docker build là gì? Yêu cầu Docker tạo image theo Dockerfile. -t là viết tắt của tag, gán tên và nhãn phiên bản. docker-flask:1.0 hoặc docker-flask:2.0 gồm tên docker-flask và tag sau dấu hai chấm. Tag là nhãn do bạn đặt; Docker không tự hiểu rằng 2.0 là bản nâng cấp nếu bạn chưa build và chạy đúng image.
Dấu chấm cuối vì sao quan trọng? Nó chọn thư mục hiện tại làm build context. Dockerfile dùng COPY để lấy file từ context này, nên cd và pwd giúp chắc chắn bạn đang chọn đúng dự án. Với lab, Docker mặc định tìm Dockerfile tại gốc context.
Khi build xảy ra điều gì? Docker chuẩn bị image nền, cài Flask và copy source theo từng bước. Cache có thể giúp bỏ qua bước không thay đổi. Build chỉ tạo image; không tự thay thế container đang chạy. Chỉ chuyển sang bước chạy hoặc thay container khi build kết thúc không lỗi.
Chỉ sau khi build thành công, thay container:
Nơi thực hiện: terminal Ubuntu WSL, thư mục ~/docker-flask (ví dụ /home/tenban/docker-flask).
cd ~/docker-flask
pwd
sudo docker stop flask-web
sudo docker rm flask-web
sudo docker run -d --name flask-web -p 127.0.0.1:8081:8000 docker-flask:2.0
Tại sao phải theo thứ tự stop, rm rồi run? stop yêu cầu ứng dụng dừng và chờ trước khi buộc dừng nếu cần; rm xóa container đã dừng, giải phóng tên flask-web; run tạo container mới từ image 2.0. Container cũ không tự chuyển sang image mới chỉ vì bạn vừa build.
Lệnh run khôi phục những gì? --name đặt lại tên, -d chạy nền và -p khai báo lại ánh xạ cổng. Các cấu hình này thuộc container nên cần truyền khi tạo mới. Trong lab, file ứng dụng đã nằm trong image; không có database hoặc dữ liệu người dùng cần chuyển sang bản mới.
Điều gì được giữ? rm không xóa source trong ~/docker-flask và không xóa image. Tuy nhiên thay đổi chỉ ghi trong lớp file của container cũ sẽ mất khi xóa container. Vì vậy sửa source ở Ubuntu rồi build image, thay vì chỉnh trực tiếp container để lưu phiên bản ứng dụng.
Refresh hoặc Ctrl+F5 để thấy Phiên bản 2.0. Với COPY trong lab, sửa code cần build image và tạo container mới. docker restart không copy source mới vào container.
11. Dừng, chạy lại và dọn dẹp
Nơi thực hiện: terminal Ubuntu WSL, thư mục ~/docker-flask (ví dụ /home/tenban/docker-flask). Các lệnh docker ps, logs, exec, run, stop, start và rm trong lab này có thể chạy ở bất kỳ thư mục Ubuntu nào vì chúng gọi Docker Engine; giữ tại thư mục dự án để dễ theo dõi.
sudo docker stop flask-web
sudo docker ps -a
sudo docker start flask-web
stop, ps -a và start khác nhau thế nào? stop dừng container; ps -a liệt kê cả container đang chạy và đã dừng; start chạy lại container đã có, giữ image và cấu hình cổng cũ.
Tại sao dùng start thay cho run? Bạn đang muốn bật lại cùng container. run sẽ tạo container mới và có thể lỗi trùng tên. Trong lúc container dừng, Flask không còn phục vụ website; sau start, thử mở trang để kiểm tra.
stop dừng container; ps -a hiển thị cả container đã dừng; start khởi động container cũ. run tạo container mới, nên không dùng lại cùng tên khi container đó còn tồn tại.
Khi kết thúc thực hành và muốn xóa container này:
Nơi thực hiện: terminal Ubuntu WSL, thư mục ~/docker-flask (ví dụ /home/tenban/docker-flask). Các lệnh docker ps, logs, exec, run, stop, start và rm trong lab này có thể chạy ở bất kỳ thư mục Ubuntu nào vì chúng gọi Docker Engine; giữ tại thư mục dự án để dễ theo dõi.
sudo docker stop flask-web
sudo docker rm flask-web
Vì sao dừng trước khi xóa? docker rm thông thường chỉ xóa container đã dừng. stop cho ứng dụng cơ hội kết thúc, rồi rm loại container khỏi Docker Engine. Sau rm, không thể start tên đó cho đến khi tạo lại bằng run.
Xóa container có xóa hết dự án không? Không. Source trên Ubuntu và image còn. File chỉ tồn tại trong lớp ghi của container sẽ bị mất cùng container. Lab chưa tạo dữ liệu người dùng; chỉ thực hiện khi đã kết thúc hoặc chuẩn bị thay container.
Source code trong Ubuntu và các image vẫn còn. Nếu WSL bị tắt hoàn toàn và Docker chưa tự chạy lại, dùng sudo service docker start trước khi chạy lệnh Docker.
12. Lỗi thường gặp
Không tìm thấy Docker hoặc không kết nối được Engine
docker: command not found: kiểm tra bước cài đặt. Cannot connect to the Docker daemon: chạy sudo service docker start, hoặc sudo systemctl start docker nếu có systemd, rồi sudo docker version. permission denied với Docker socket: giữ sudo trước lệnh Docker.
Build thất bại hoặc thiếu Dockerfile
Nơi thực hiện: terminal Ubuntu WSL, thư mục ~/docker-flask (ví dụ /home/tenban/docker-flask). Các lệnh docker ps, logs, exec, run, stop, start và rm trong lab này có thể chạy ở bất kỳ thư mục Ubuntu nào vì chúng gọi Docker Engine; giữ tại thư mục dự án để dễ theo dõi.
pwd
ls -la
cd ~/docker-flask
Tại sao kiểm tra pwd trước? Build dùng dấu chấm làm context, nên thư mục hiện tại quyết định Docker đọc những file nào. ls -la cho thấy cả Dockerfile và file ẩn. cd ~/docker-flask đưa bạn về đúng gốc dự án trước khi thử lại.
Kết quả đúng: Dockerfile, app.py, requirements.txt nằm tại gốc; index.html nằm trong templates. Tên có phân biệt chữ hoa và thường trên Linux.
Đặt đúng tên Dockerfile, giữ dấu chấm cuối lệnh build. Lỗi tải image hoặc pip thường liên quan Internet, DNS hoặc proxy; kiểm tra thông báo lỗi trước khi thử lại.
Container thoát hoặc trang trả lỗi 500
Nơi thực hiện: terminal Ubuntu WSL, thư mục ~/docker-flask (ví dụ /home/tenban/docker-flask). Các lệnh docker ps, logs, exec, run, stop, start và rm trong lab này có thể chạy ở bất kỳ thư mục Ubuntu nào vì chúng gọi Docker Engine; giữ tại thư mục dự án để dễ theo dõi.
sudo docker ps -a
sudo docker logs flask-web
docker logs là gì? Đọc dữ liệu stdout và stderr mà ứng dụng trong container ghi ra. flask-web xác định container cần xem. -f nghĩa là follow, tiếp tục theo dõi các dòng log mới thay vì xem một lần rồi kết thúc.
Tại sao xem log? Log giúp biết Flask đã khởi động chưa, có lỗi Python hay không và yêu cầu HTTP nhận mã gì. GET / với 200 cho biết trang chủ trả thành công. Ctrl+C khi theo dõi logs chỉ ngắt lệnh xem log, không dừng container.
IndentationError: sửa thụt dòng Python. ModuleNotFoundError: kiểm tra requirements và bước pip install. TemplateNotFound: kiểm tra templates/index.html và COPY trong Dockerfile. Sửa source rồi build và tạo lại container.
Tên container đã tồn tại
Dùng sudo docker ps -a. Nếu muốn chạy lại container cũ, dùng start. Nếu muốn thay phiên bản, stop và rm đúng flask-web rồi tạo lại. Không cần xóa các container khác.
Cổng 8081 bị chiếm
Nơi thực hiện: terminal Ubuntu WSL, thư mục ~/docker-flask (ví dụ /home/tenban/docker-flask). Các lệnh docker ps, logs, exec, run, stop, start và rm trong lab này có thể chạy ở bất kỳ thư mục Ubuntu nào vì chúng gọi Docker Engine; giữ tại thư mục dự án để dễ theo dõi.
sudo docker run -d --name flask-web-8082 -p 127.0.0.1:8082:8000 docker-flask:2.0
docker run là gì? Tạo một container mới từ image và khởi động nó. Docker dùng image có sẵn; nếu tag không có cục bộ, Docker có thể thử tải từ registry. Vì vậy cần dùng đúng tên và tag đã build.
-d và --name có ý nghĩa gì? -d chạy tách khỏi terminal để bạn tiếp tục gõ lệnh. --name gán tên dễ nhớ như flask-web hoặc flask-web-8082 để dùng với logs, stop và exec. Một tên container phải duy nhất trong cùng Docker Engine, kể cả khi container đã dừng.
-p 127.0.0.1:8081:8000 đọc thế nào? Ba phần lần lượt là địa chỉ bind phía máy chạy Docker, cổng ngoài và cổng container. Trình duyệt dùng localhost:8081; Flask vẫn nghe 8000. Khi ví dụ đổi thành 8082:8000, chỉ địa chỉ truy cập bên ngoài đổi sang 8082. Bind 127.0.0.1 giới hạn cổng ở localhost phía Docker host.
Tại sao cần publish cổng? Ứng dụng có mạng riêng trong container; -p tạo điểm truy cập từ bên ngoài. EXPOSE trong Dockerfile chưa làm việc này. Với WSL có chuyển tiếp localhost phù hợp, trình duyệt Windows truy cập được qua localhost.
Vì sao container tiếp tục chạy? CMD chạy Flask ở foreground bên trong container, dù Docker được gọi với -d từ terminal. Tiến trình chính còn chạy thì container còn hoạt động; nếu Python lỗi hoặc kết thúc, container sẽ dừng.
Mở http://localhost:8082. Nếu mới build tag 1.0, dùng docker-flask:1.0 ở cuối lệnh. Cổng Flask bên trong vẫn là 8000.
Ubuntu truy cập được nhưng Windows chưa mở được
Nơi thực hiện: terminal Ubuntu WSL, thư mục ~/docker-flask (ví dụ /home/tenban/docker-flask). Các lệnh docker ps, logs, exec, run, stop, start và rm trong lab này có thể chạy ở bất kỳ thư mục Ubuntu nào vì chúng gọi Docker Engine; giữ tại thư mục dự án để dễ theo dõi.
curl -I http://localhost:8081
sudo docker port flask-web
curl -I là gì? curl gửi yêu cầu HTTP; -I yêu cầu phần header bằng phương thức HEAD, giúp kiểm tra nhanh trạng thái trả về. HTTP 200 xác nhận server phản hồi; chưa đủ để kết luận giao diện hiển thị đúng, nên vẫn cần mở trình duyệt.
docker port flask-web là gì? Hiển thị các ánh xạ cổng đã cấu hình cho container. Dùng nó để kiểm tra cổng ngoài thực tế trước khi gõ địa chỉ trên Windows. Nếu Ubuntu truy cập được nhưng Windows không được, cần kiểm tra tầng chuyển tiếp mạng WSL.
Nếu Ubuntu nhận HTTP 200, thử http://127.0.0.1:8081 trên Windows. Kiểm tra đúng cổng, proxy, VPN, firewall và cấu hình localhost forwarding của WSL; không cần tắt toàn bộ firewall.
13. Bài tập và tiêu chí hoàn thành
Bài 1: đổi tiêu đề thành Docker Lab của bạn và viết lại phần giới thiệu. Bài 2: build tag 3.0, tạo container flask-practice ở cổng ngoài 9090 và cổng trong 8000. Bài 3: stop rồi start container và quan sát website. Bài 4: giải thích vì sao sửa source chưa tự thay đổi container.
Hoàn thành khi: tự build thành công, thấy container Up, mở được website trên Windows, xem log HTTP 200 và cập nhật được phiên bản mới. Bạn cần phân biệt source code, Dockerfile, image và container bằng chính dự án vừa thực hiện.
14. Khi triển khai thực tế
Server tích hợp của Flask dùng cho lab và phát triển. Triển khai thực tế cần WSGI server như Gunicorn, người dùng không phải root, quản lý secret ngoài source code, phiên bản thư viện đã kiểm thử, HTTPS và kiểm tra sức khỏe. Lab không triển khai database, volume hoặc đẩy image lên registry; các thành phần này được giới thiệu thêm ở bài Airflow Lab.
TechData.AI
