Một bản ghi chép nhỏ về Docker

  1. 1. Docker Command Line
    1. 1.1. Thao tác với mỗi lần chạy:
    2. 1.2. -i và -t flag:
    3. 1.3. Docker container và -d flag:
    4. 1.4. –name flag:
    5. 1.5. -p flag:
    6. 1.6. Docker log:
    7. 1.7. Docker pull:
    8. 1.8. Docker commit:
    9. 1.9. Docker build command
    10. 1.10. Docker inspect:
    11. 1.11. Gỡ Image và Container:
      1. 1.11.1. Gỡ container:
      2. 1.11.2. Gỡ image:
  2. 2. Docker Container’s Filesystem
    1. 2.1. Docker images và layers:
    2. 2.2. Docker container và writable layer:
  3. 3. Cách build một Docker image:
    1. 3.1. Build Docker Images by using Docker Commit Command:
    2. 3.2. Build Docker Images by writing DockerFile:
      1. 3.2.1. Dockerfile là gì?
      2. 3.2.2. Viết một Dockerfile:
        1. 3.2.2.1. Câu lệnh RUN:
        2. 3.2.2.2. Câu lệnh CMD:
        3. 3.2.2.3. Câu lệnh ENTRYPOINT
        4. 3.2.2.4. Docker cache:
        5. 3.2.2.5. Câu lệnh COPY:
        6. 3.2.2.6. Câu lệnh ADD:
        7. 3.2.2.7. Câu lệnh WORKDIR:
        8. 3.2.2.8. Xây dựng Flask web đơn giản qua Dockerfile:
        9. 3.2.2.9. Docker Container Links:
        10. 3.2.2.10. Docker compose

Docker Command Line

Thao tác với mỗi lần chạy:

1
2
3
docker run busybox:1.24 echo "hello world!"

docker run busybox:1.24 ls /

-i và -t flag:

Flag -i dùng để mở một tương tác với container, giữ cho nó tiếp tục được mở để thực thi những tác vụ tiếp.
Flag -t dùng để tạo một pseudo-TTY(TTY tương tự như shell command trong linux), có nghĩa là tạo một shell của image rồi gắn vào terminal của mình.

1
2
3
4
5
6
7
docker run -i -t busybox:1.24
hay
docker run -it busybox:1.24
Kết quả:
> docker run -it busybox:1.24
/ # ls
bin dev etc home proc root sys tmp usr var

Docker container và -d flag:

Khi chạy một cách bình thường, nó sẽ thực thi lệnh trực tiếp trên terminal của chúng ta (run container in foreground). Nhưng nếu chúng ta muốn nó không chạy trên terminal mà chạy ở trong container (run container in background).
Ví dụ khi chạy lệnh sleep 1000:

1
2
3
4
5
6
7
8
Không chạy với tag -d:
docker run busybox:1.24 sleep 1000
nó sẽ sleep trực tiếp trên terminal(mở terminal mới đi bro :P)
Chạy với tag -d:
docker run -d busybox:1.24 sleep 1000
> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4b69615931bb busybox:1.24 "sleep 1000" 7 seconds ago Up 6 seconds eloquent_newton

Để xem container đang chạy gõ docker ps(chạy trong background hoặc trên terminal), với container đã dừng gõ docker ps -a.
Để không lưu lại container khi kết thúc docker container chúng ta dùng tag –rm, khi đó nếu quy trình kết thúc, docker sẽ tự động xóa container mà không lưu vào docker ps -a.

–name flag:

Đặt tên cho container mỗi lần chạy.

1
docker run --name hello_world busybox:1.24

-p flag:

-p host_port:container_port : map cổng của container sang cổng máy thật. Chúng ta có thể map nhiều cặp port một lần bằng cách viết liên tiếp những cặp đó -p 8888:8080 80:4444.
Ví dụ với tomcat server, tomcat là một open source web server thực thi Java servlet. Tomcat image này chạy ở port 8080:

1
docker run -it -p 8888:8080 tomcat:8.0

Đợi một lát để nó pull tomcat:8.0 về.
Sau khi tải xong truy cập http://localhost:8888/ trên browser. Tomcat đã chạy trên port 8888.
Mình sẽ giải thích một chút:

  • Tomcat này là một container (xem như là một máy ảo cho dễ hình dung nhưng hầu như không phải nhé ^^), nó chạy trên port 8080 của máy chủ tomcat này.
  • Sau khi map qua cổng của localhost, cổng 8888 bây giờ sẽ như là cổng 8080 của máy chủ tomcat vậy.

Docker log:

Khi chạy một server qua terminal, nhật kí sẽ được ghi lại trực tiếp ở trên terminal. Khi chạy server qua container background để xem nhật kí của server chúng ta sử dụng câu lệnh docker logs

1
2
3
4
5
6
7
8
9
10
> docker run -it -d -p 8888:8080 tomcat:8.0
4cc4583095b59189adaa0dc202944d5cdd5968df403584a84b26d6e57c585bd7
pwn@DESKTOP-AC6UABE:pts/3->/home/pwn (0)
> docker logs 4cc4583095b59189adaa0dc202944d5cdd5968df403584a84b26d6e57c585bd7
Using CATALINA_BASE: /usr/local/tomcat
Using CATALINA_HOME: /usr/local/tomcat
Using CATALINA_TMPDIR: /usr/local/tomcat/temp
Using JRE_HOME: /docker-java-home/jre
......phần log ở dưới dài nên mình cắt......
Hoặc lấy id từ docker ps rồi docker logs <id>

Docker pull:

Câu lệnh này dùng để lấy image từ trên hub của docker về. Ví dụ:

1
2
docker pull mysql:lastest
docker pull nginx

Docker commit:

Lệnh docker commit dùng để lưu những thay đổi trong file system của một Docker container vào image mới.
Syntax: docker commit container_id repository_name:tag

Docker build command

Câu lệnh docker build dùng để build một image từ một Dockerfile và context (nơi chứa những thứ như file php, java,… cần dùng để build). Hai thứ này được chứa trong cùng một folder, chúng ta gắn URL hoặc path để chỉ cho docker biết nơi chứa 2 cái này.
Systax:

1
2
3
docker build [OPTIONS] PATH
Ví dụ:
docker build -t image_name .
  • -t flag dùng để đặt tên cho image.
  • Dấu . để nói cho docker biết là tìm Dockerfile ở thư mục hiện tại, đây là path dẫn đến Dockerfile.

Docker inspect:

Mô tả thông tin low level của một container hay image.

1
2
3
4
> docker run -d busybox:1.24 sleep 100
0c06e0155702fcb6dc7969eac63f68d74f5781dbc6960461ee88dc3c15942ea9
pwn@DESKTOP-AC6UABE:pts/4->/home/pwn (0)
> docker inspect 0c06e0155702fcb6dc7969eac63f68d74f5781dbc6960461ee88dc3c15942ea9

Gỡ Image và Container:

Gỡ container:

  • Đầu tiên phải dừng container đang chạy: docker stop <id container>
  • docker ps -a để xem container đã dừng.
  • docker rm -f <id container đã dừng>, -f flag ở đây là cờ bắt buộc gỡ, nên xài để gỡ thuận tiện hơn.

Gỡ image:

  • docker rmi <tên image>

Docker Container’s Filesystem

Docker images và layers:

Một Docker images được xây dựng từ những layer. Mỗi layer đại diện cho mỗi lệnh trong Dockerfile, hay khi chạy lệnh bằng cách build thông qua Docker Commint Command ở phần sau.
Mỗi câu lệnh có thay đổi file của một container đều tạo ra một layer mới. Để dễ hình dung chúng ta cùng đi qua ví dụ sau(Lấy từ docs.docker.com):

1
2
3
4
5
6
FROM ubuntu:18.04
LABEL org.opencontainers.image.authors="org@example.com"
COPY . /app
RUN make /app
RUN rm -r $HOME/.cache
CMD python /app/app.py

Khi chạy file trên, nó sẽ tạo ra một Docker image mới có cấu hình theo các câu lệnh được chạy ở file trên. Câu lệnh LABEL không liên quan đến file hệ thống. Câu lệnh COPY thêm file ở thư mục chứa Dockerfile vào thư mục /app của container. Tuy nhiên nó chỉ COPY trong quá trình chạy, để viết thay đổi đó vào layer của một image mới chúng ta cần chạy lệnh RUN make /app, khi này một layer mới mới được tạo. RUN rm -r $HOME/.cache gỡ thư mục cache, câu lệnh RUN này sẽ tạo ra một layer mới. Câu lệnh CMD cuối cùng chỉ để chạy command python khi container đang chạy và không tạo layer mới.

Để xem các layers của một image chúng ta dùng lệnh docker image history <id>

Docker container và writable layer:

Docker container được tạo từ những docker image chỉ đọc hay không thay đổi được. Một docker container được tạo từ một image sẽ có thêm một lớp ở trên, lớp này được gọi là writable layerhay container layer. Mọi thay đổi trong container này sẽ được lưu vào lớp này.

Khi nghiên cứu tới đây, mình có một thắc mắc là như đã nói, khi tạo một container việc thay đổi trong container này chỉ thay đổi lớp writable, rồi một image là không thay đổi được vậy thì tại sao chúng ta có thể thay đổi file hệ thống khi chạy container?. Theo mình hiểu thì nó như này:

  • Khi chúng ta tạo một container, nó như là một máy mới được tạo ra vậy, chúng ta có thể thay đổi các file, folder trong máy mới này. Nó sẽ không ảnh hưởng gì đến các image cả vì nó riêng biệt mà.
  • Thế rồi writable đó để làm gì? Khi thay đổi một container chạy từ một image rồi lưu lại thành một image mới, người ta sẽ lưu lại quy trình theo từng bước khiến file hệ thống bị thay đổi để tạo quá trình build image mới này có trình tự, và chắc cũng dễ dàng hơn. Và khi này những bước thay đổi đó sẽ được lưu xuống lớp writable, nếu chúng ta commit thay đổi này, những cái thay đổi thực hiện ghi lại trong lớp writable này sẽ được lưu lại theo những layer mới xếp chồng lên những layer của image cũ.

Cách build một Docker image:

Có hai cách để build một docker image:

  • Commit những cái đã build, thay đổi trong một Docker container vào một image mới.
  • Viết một Dockerfile.
    Bây giờ chúng ta cùng đi cụ thể vào từng cách để build một docker image.

Build Docker Images by using Docker Commit Command:

Giả sử chúng ta chúng ta có một base image là một debian (hệ điều hành tương tự linux), khi run image này nó không có Git command. Bây giờ để thuận tiện hơn chúng ta tạo một image mới có cài sẵn Git dựa trên debian image kia để chúng ta không phải cài lại Git mỗi lần chạy. Khi đó chúng ta có ba bước để thực hiện như sau:

    1. Tạo một container từ base image
    1. Install Git pakage trong container.
    1. Commit thay đổi trong container đã làm.
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      # Pull và chạy debian
      docker run -it debian:jessie
      # Cài Git cho container này
      apt-get update && apt-get install --force-yes git
      # Commit thay đổi, gõ "docker ps -a" để lấy id của container này.
      docker commit ba638bca3016 rayinaw/debian:update
      # Trả về sha256:3e6332e9ae0a1597a25d213bf42b0592e3291969259d631c3eacc893f4735171
      Gõ docker images để thấy một image mới đã được tạo. Size nó lớn hơn debian cũ 100 mb, là dung lượng của git.
      Bây giờ chạy để test.
      docker run -it rayinaw/debian:update
      Gõ git trong terminal của debian chúng ta tạo, git đã được cài vào image mới thành công.

Build Docker Images by writing DockerFile:

Dockerfile là gì?

Dockerfile là một file văn bản chứa những lệnh user cung cấp để build các image một cách tự động từ Dockerfile. Tên của Dockerfile phải là Dockerfile với D viết hoa ở đầu.
Mỗi lệnh trong Dockerfile sẽ tạo một image layer mới đối với image này. Các lệnh sẽ chỉ định điều cần làm khi building image.
Khi building image, Docker sẽ thực hiện quy trình tương tự như việc chúng ta build bằng commit command. Docker sẽ build lần lượt từng layer, khi build xong ở trong lớp writable container, nó sẽ viết xuống image mới, sau đó remove container đó, rồi lần lượt theo trình tự đó cho đến khi build đủ các layer trong Dockerfile. Việc viết xuống image mình không biết nó lưu ở đâu, nếu muốn tìm hiểu các bạn có thể tìm hiểu thêm về Docker daemon.

Viết một Dockerfile:

Câu lệnh đầu tiên trong Dockerfile là câu lệnh FROM (viết hoa để phân biệt với from arguments), dùng để chỉ định base image.

Câu lệnh RUN:

  • Dùng để chỉ định câu lệnh thực thi khi chạy base image.
  • Câu lệnh RUN sẽ thực thi câu lệnh trong writable layer của container, sau đó commit container xuống image mới.
  • Image mới này sẽ được sử dụng cho bước tiếp theo trong Dockerfile. Vì vậy mỗi lần RUN lệnh, nó sẽ tạo ra một image layer mới.
    Ví dụ tương tự như cách build qua Docker commit:
    1
    2
    3
    4
    5
    6
    # Đặt tên file là Dockerfile nhé.
    # Lưu ý phiên bản ubuntu tạo phải được hỗ trợ, nếu không khi chạy lệnh update nó sẽ báo lỗi. Mình chưa biết fix nên không nói rõ nhé :).
    FROM ubuntu:20.04
    RUN apt-get update
    RUN apt-get install -y git
    RUN apt-get install -y vim
    Bây giờ truy cập vào folder chứa Dockerfile rồi gõ lệnh docker build -t ubuntu:own .

Viết gọn các câu lệnh trên:

1
2
3
4
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y \
git \
vim

Câu lệnh CMD:

Khác với RUN, câu lệnh CMD sẽ không thực thi trong quá trình build image, nó chỉ thực thi khi khởi chạy container của image đó.

Cái này nhìn ví dụ là dễ hiểu nhất:

1
2
3
4
5
FROM ubuntu:20.04
RUN apt-get -y update
RUN apt-get install -y git
RUN apt-get install -y vim
CMD ["ls", "/"]

Chạy docker build -t test . để build image.

Bây giờ chạy docker run test nó sẽ thực thi lệnh bên trong container này. Nó tương ứng với lúc chúng ta chạy lệnh docker run test ls /.

Nếu chúng ta chạy lệnh trực tiếp trên command line như docker run test echo "hello world" thì nó chỉ thực thi lệnh này và bỏ qua lệnh được viết ở CMD trong Dockerfile.

Câu lệnh ENTRYPOINT

Như ví dụ ở câu lệnh CMD, nếu chúng ta thực hiện docker run test echo "hello world" thì nó sẽ bỏ qua câu lệnh ls /.

Đó là điều mà ta không mong muốn nếu như build image mà lỡ có ai mới học chạy chương trình của ta không được :).

Do đó lệnh ENTRYPOINT giúp ta tránh điều đó:

1
2
3
4
5
FROM ubuntu:20.04
RUN apt-get -y update
RUN apt-get install -y git
RUN apt-get install -y vim
ENTRYPOINT ["ls", "/"]

Bây giờ nếu chạy docker run test echo "hello world" thì nó vẫn chỉ chạy câu lệnh của ENTRYPOINT mà không chạy câu lệnh đăng sau docker run bởi vì nó là điểm cuối rồi.

Docker cache:

1
2
3
4
FROM ubuntu:20.04
RUN apt-get -y update
RUN apt-get install -y git
RUN apt-get install -y vim

Nếu chúng ta build Dockerfile trên từ lần thứ 2 trở đi nó sẽ build nhanh hơn lần đầu tiên, bởi vì nó sử dụng lại layer đã build. Cái này liên quan đến docker cache. Mỗi layer riêng lẻ build ở trong mỗi lần build image, nó đều được ghi lại ở docker cache để sử dụng lại (riêng lẻ luôn chứ không phải sử dụng theo từng image).

Ví dụ:
Build image 1:

1
2
3
FROM ubuntu:20.04          # --> cache
RUN apt-get -y update # --> cache
RUN apt-get install -y git # --> cache

Build image 2:

1
2
3
FROM ubuntu:20.04          # --> reuse
RUN apt-get -y update # --> reuse
RUN apt-get install -y vim # --> Câu lệnh này khác và layer tương ứng với câu lệnh này chưa được tạo nên sẽ thực hiện tạo layer mới và đương nhiên sẽ cache lại.

Để không cache lại, chúng ta có thể sử dụng cờ --no-cache=true, mà cờ này hơi phế nhỉ, xài làm gì :).
docker build -t test . --no-cache=true

Câu lệnh COPY:

Dùng để copy file hay thư mục từ build context (nơi chứa Dockerfile và những file cần thiết) vào file hệ thống của container. Ví dụ:
Nội dung Dockerfile:

1
2
3
4
5
FROM ubuntu:20.04
RUN apt-get -y update
RUN apt-get install -y vim
COPY abc.txt /src/abc.txt
CMD ["cat", "/src/abc.txt"]

Cùng thư mục với Dockerfile, tạo một file abc.txt ghi nội dung gì đó để test~

1
2
3
docker build -t addfile .
> docker run addfile
abc.txt file ne

File abc.txt đã được copy vào thư mục src của container.

Câu lệnh ADD:

Câu lệnh này gần như tương tự với câu lệnh COPY, điểm khác ở đây là ADD không chỉ copy từ context mà còn có thể download từ internet và copy vào container.

1
ADD https://example.com/big.tar.xz /usr/src/things/

ADD cũng có thể tự động giải nén file được tại về với một vài định dạng được hỗ trợ.

Câu lệnh WORKDIR:

Câu lệnh này dùng để tạo folder chỉ định và truy cập đến folder đó. WORKDIR /app

Xây dựng Flask web đơn giản qua Dockerfile:

Python image: Python image là một image được xây dựng dựa trên base image là Alpine Linux. Trong Alpine Linux này người ta sẽ cài python compiler vào. Cái này là trả lời thắc mắc cho việc tại sao python là compile mà lại có workdir các kiểu ~

Đầu tiên tạo thư mục app. Trong thư mục này sẽ chứa Dockerfile và một thư mục khác chứa file app.py, đặt là app luôn cho dễ nhé.
File app.py:

1
2
3
4
5
6
7
8
9
from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
return 'hello world!'

if __name__ = '__main__':
app.run(host = '0.0.0.0')

Nội dung Dockerfile:

1
2
3
4
5
FROM python:3.8-alpine
RUN pip install Flask==2.0.2
WORKDIR /app
COPY app /app
CMD ["python", "app.py"]

Build image: docker build -t flaskweb .
Chạy web: docker run -d -p 5000:5000 flaskweb
Bây giờ trên browser gõ http://localhost:5000 để truy cập web vừa chạy.

Container links cho phép các container tìm ra nhau và trao đổi thông tin một cách bảo mật với nhau.
Khi set up một cái link, chúng ta tạo ra một ống dẫn giữa source container và recipient container. Recipient container có thể truy cập, lấy dữ liệu và thêm dữ liệu từ source container.
Links được thành lập bằng cách sử dụng container name. Lợi ích của việc này là chúng ta có thể thấy một cách rõ ràng hơn, không cần phải thông qua localhost.
Ví dụ xây dựng một web app từ flask và redis (api để lưu trữ dữ liệu):

1
2
3
4
5
Cây thư mục như sau:
Dockerfile
app -------->app.py
|
--->templates----->index.html

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<html>

<head>
<title>simple service</title>
</head>

<body>
<form method="post">
<input type="text" name="key" value={{ key }}>
<input type="text" name="cache_value" value={{ cahe_value }}>
<input type="submit" name="submit" value="load">
<input type="submit" name="submit" value="save">
</form>
</body>

</html>

app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from flask import Flask, request, render_template
import redis
app = Flask(__name__)
default_key = '1'
cache = redis.StrictRedis(host='redis', port=6379, db=0)
cache.set(default_key, "one")

@app.route('/', methods=['GET', 'POST'])
def main():
key = default_key
if key in request.form:
key = request.form['form']
if request.method == 'POST' and request.form['submit'] == 'save':
cache.set(key, request.form['cache_value'])
cache_value = None
if cache.get(key):
cache_value=cache.get('key').decode('utf-8')
return render_template('index.html', key=key, cache_value=cache_value)

if __name__ == '__main__':
app.run(host='0.0.0.0')

Dockerfile

1
2
3
4
5
FROM python:3.8-alpine
RUN pip install Flask==2.0.2 redis==2.10.5
WORKDIR /app
COPY app /app
CMD ["python", "app.py"]

Pull và khởi chạy redis: docker run -d --name redis redis:3.2.0
Build image: docker build -t dockerapp:v0.1 .
Khởi chạy image và link với redis: docker run -d -p 5000:5000 --link redis dockerapp:v0.1

Docker compose

Các thuộc tín trong docker compose:

  • version: chỉ ra phiên bản docker-compose đã sử dụng.
  • services: thiết lập các services(containers) muốn cài đặt và chạy.
  • image: chỉ ra image được sử dụng trong lúc tạo ra container.
  • build: định nghĩa một môi trường xây dựng (build context) cho Docker image và các tham số liên quan đến quá trình xây dựng. Các trường con:
    • Nếu muốn chỉ rõ, ta có thể sử dụng đến trường con context
    • dockerfile: Xác định đường dẫn đến Dockerfile trong build context
  • ports: thiết lập ports chạy tại máy host và trong container.
  • restart: tự động khởi chạy khi container bị tắt.
  • environment: thiết lập biến môi trường ( thường sử dụng trong lúc config các thông số của db).
  • depends_on: chỉ ra sự phụ thuộc. Tức là services nào phải được cài đặt và chạy trước thì service được config tại đó mới được chạy.
  • volumes: dùng để mount hai thư mục trên host và container với nhau.

Cũng như ví dụ ở trên, thay vì chúng ta pull, build image rồi khởi chạy rườm rà. Chúng ta có thể viết một docker compose rồi khởi chạy chỉ với một câu lệnh docker-compose up.

Docker compose file cần tuân thủ nghiêm ngặt thụt lề, mỗi lần thụt là 2 khoảng trắng.

File name: docker-compose.yml

  • Dockerfile phải đặt chung với context của trường build (ở đây là .)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    version: '3'

    services:
    dockerapp:
    build: .
    ports:
    - "5000:5000"
    depends_on:
    - redis
    redis:
    image: redis:3.2.0
    Khởi chạy gõ docker-compose up hoặc thêm cờ -d docker-compose up -d để treo trong container.

Docker compose sẽ không build lại image nếu image đó đã tồn tại, vì vậy khi ta thay đổi một vài lệnh trong Dockerfile docker-compose vẫn lấy image cũ để thao tác.

Để build lại một image mới, chúng ta cần sử dụng lệnh docker-compose build.