Search K
Appearance
Appearance
在使用 Docker 时,我们经常会使用到镜像来部署应用程序。Docker 提供了丰富的官方镜像,但有时候我们需要根据特定需求来定制自己的镜像。
Overview of best practices for writing Dockerfiles | Docker Docs
首先,我们需要创建一个 Dockerfile 文件来定义我们的自定义镜像。Dockerfile 中包含了构建镜像的各个步骤。
# 使用官方 ubuntu 镜像作为基础镜像
FROM ubuntu:latest
# 设置镜像的维护者信息
MAINTAINER Your Name <your@email.com>
# 执行更新和安装所需软件
RUN apt-get update && apt-get install -y \
software-properties-common \
python3 \
python3-pip
# 在镜像中创建一个新目录
RUN mkdir -p /myapp
# 设置工作目录
WORKDIR /myapp
# 将本地的 app 文件复制到镜像的 /myapp 目录中
COPY app /myapp
# 设置环境变量
ENV APP_ENV=production
# 暴露容器的端口
EXPOSE 8080
# 配置容器启动后执行的命令
CMD ["python3", "app.py"]上面的 Dockerfile 中包含了几个常用的指令:
FROM 指定了基础镜像:使用了最新版的 Ubuntu 作为基础镜像。MAINTAINER 设置了镜像的维护者信息。RUN 在镜像中执行命令:更新并安装了所需的软件包。WORKDIR 设置工作目录。COPY 将本地文件复制到镜像中。WORKDIR 设置工作目录。ENV 定义了环境变量:环境变量 APP_ENV。EXPOSE 暴露容器内部端口:8080。CMD 配置容器启动后执行的命令。在创建好 Dockerfile 后,我们可以使用 docker build 命令来构建镜像。
docker build -t custom_image:latest .-t custom_image:latest 参数指定指定镜像名称和标签。. 表示 Dockerfile 所在的目录。构建好镜像后,我们就可以使用它来创建容器了。
docker run -d -p 8080:8080 --name custom_container custom_image:latest-d 参数表示在后台运行容器。-p 8080:8080 指定了端口映射,将容器的 8080 端口映射到宿主机的 8080 端口。--name custom_container 指定了容器的名称。custom_image 是我们构建的自定义镜像的名称。选择合适的基础镜像是构建自定义 Docker 镜像的第一步。常见的选择包括 Alpine、Ubuntu、CentOS 等。
scratch 是一个特殊的基础镜像,它实际上是一个空白镜像,不包含任何文件系统内容。因此,使用 scratch 作为基础镜像意味着你从头开始构建你的镜像,而不是基于其他镜像。
Alpine Linux 是一个轻量级的基础镜像,对于容器来说是一个理想的选择,因为它非常小巧。镜像特点如下:
# 使用 Alpine Linux 作为基础镜像
FROM alpine:latest
# 安装应用程序或服务
RUN apk --no-cache add \
nginx
# 配置应用程序或服务
COPY nginx.conf /etc/nginx/nginx.conf
# 定义容器启动时执行的命令
CMD ["nginx", "-g", "daemon off;"]FROM nginx:1.25.3-alpine
CMD ["/bin/bash"]
MAINTAINER "Ryanjie" <54110@qq.com>
ENV TERM=xterm-256color LANG=en_US.UTF-8 LANGUAGE=en_US.UTF-8 LC_ALL=en_US.UTF-8 TZ=Asia/Shanghai
WORKDIR /usr/share/nginx/html
# Copy static assets
COPY ./dist /usr/share/nginx/html
# Copy the nginx configuration file
COPY ./nginx.conf /etc/nginx/nginx.conf
# Expose port 80
EXPOSE 80
# Start nginx
CMD ["nginx", "-g", "daemon off;"]FROM nginx:1.25.3
CMD ["/bin/bash"]
MAINTAINER "Ryanjie" <54110@qq.com>
ENV TERM=xterm-256color LANG=en_US.UTF-8 LANGUAGE=en_US.UTF-8 LC_ALL=en_US.UTF-8 TZ=Asia/Shanghai
WORKDIR /usr/share/nginx/html
# Copy static assets
COPY ./dist /usr/share/nginx/html
# Copy the nginx configuration file
COPY ./nginx.conf /etc/nginx/nginx.conf
# Expose port 80
EXPOSE 80
# Start nginx
CMD ["nginx", "-g", "daemon off;"]❯ tree
.
├── dist
│ └── index.html
├── Dockerfile-nginx-debian
├── Dockerfile-nginx-alpine
└── nginx.conf
1 directory, 4 files
❯ docker build -t nginx:debian -f Dockerfile-nginx-debian .
[+] Building 0.9s (8/8) FINISHED docker:default
=> [internal] load build definition from Dockerfile-nginx 0.0s
=> => transferring dockerfile: 431B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/nginx:1.25.3 0.7s
=> [1/3] FROM docker.io/library/nginx:1.25.3@sha256:86e53c4c16a6a276b204b0fd3a8143d86547c967dc8258b3d47c3a21bb68d3c6 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 91B 0.0s
=> CACHED [2/3] COPY ./dist /usr/share/nginx/html 0.0s
=> CACHED [3/3] COPY ./nginx.conf /etc/nginx/nginx.conf 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:f66dceae84527fe40b91b3df15e4fdc6b0d7328df34e884f37d5193648e03863 0.0s
=> => naming to docker.io/library/nginx:debian 0.0s
❯ docker build -t nginx:alpine -f Dockerfile-nginx-alpine .
[+] Building 0.8s (8/8) FINISHED docker:default
=> [internal] load build definition from Dockerfile-nginx-alpine 0.0s
=> => transferring dockerfile: 444B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/nginx:1.25.3-alpine 0.6s
=> [1/3] FROM docker.io/library/nginx:1.25.3-alpine@sha256:db353d0f0c479c91bd15e01fc68ed0f33d9c4c52f3415e63332c3d0bf7a4bb77 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 91B 0.0s
=> CACHED [2/3] COPY ./dist /usr/share/nginx/html 0.0s
=> CACHED [3/3] COPY ./nginx.conf /etc/nginx/nginx.conf 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:80daa91ac6a5fade97b31062f170ccffdcf47239002a4300a61e0b3da19442fe 0.0s
=> => naming to docker.io/library/nginx:alpine 0.0s
❯ docker images # 可以看到 alpine 版本的 nginx 镜像比 debian 版本的 nginx 镜像小很多
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx alpine 80daa91ac6a5 7 seconds ago 47.7MB
nginx debian f66dceae8452 35 seconds ago 187MB# 阶段1:使用 Node.js 构建应用程序
FROM node:14 as builder
WORKDIR /app
# 复制应用程序源代码到容器中
COPY . .
# 安装依赖并构建应用程序
RUN npm install
RUN npm run build
# 阶段2:创建最终镜像,只包含构建好的应用程序
FROM nginx:alpine
# 将阶段1中构建好的应用程序复制到最终镜像中
COPY --from=builder /app/dist /usr/share/nginx/html
# 非必须:如果需要自定义 Nginx 配置,可以将配置文件复制到镜像中
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 指定 Nginx 的工作目录
WORKDIR /usr/share/nginx/html
# 暴露端口
EXPOSE 80
# 容器启动时执行的命令
CMD ["nginx", "-g", "daemon off;"]as builder): 使用 Node.js 镜像,将应用程序源代码复制到容器中,安装依赖并构建应用程序。这个阶段产生一个包含构建好的应用程序的中间镜像。INFO
在使用多阶段构建时,你并不局限于从之前在 Dockerfile 中创建的阶段中复制。你可以使用 COPY --from 指令从单独的镜像复制,可以使用本地镜像名称、本地或 Docker 注册表上的标签或标签 ID。
COPY --from=nginx:1.25.3-alpine /etc/nginx/nginx.conf /nginx.confFROM golang:1.21-alpine as base
WORKDIR /src
COPY main.go /src/main.go
RUN go build -o /bin/hello ./main.go
FROM alpine:3.18 as stage1
RUN echo "Hello, stage1"
FROM as stage2
COPY --from=base /bin/hello /bin/hello
CMD ["/bin/hello"]package main
import "fmt"
func main() {
fmt.Println("hello, world")
}❯ DOCKER_BUILDKIT=1 dkb --no-cache -f Dockerfile-multi-stage --target stage2 -t multi-stage-buildkit . # 启用 BuildKit 后,在此 Dockerfile 中构建 stage2 目标,意味着只处理 base 和 stage2 。对 stage1 没有依赖性,所以跳过了它。
[+] Building 5.4s (10/10) FINISHED docker:default
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load build definition from Dockerfile-multi-stage 0.0s
=> => transferring dockerfile: 295B 0.0s
=> [internal] load metadata for docker.io/library/golang:1.21-alpine 0.8s
=> [base 1/4] FROM docker.io/library/golang:1.21-alpine@sha256:110b07af87238fbdc5f1df52b00927cf58ce3de358eeeb1854f10a8b5e5e14 0.0s
=> CACHED [base 2/4] WORKDIR /src 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 108B 0.0s
=> [base 3/4] COPY main.go /src/main.go 0.1s
=> [base 4/4] RUN go build -o /bin/hello ./main.go 4.3s
=> [stage2 1/1] COPY --from=base /bin/hello /bin/hello 0.1s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:898867bfef2b7128f729132d1486ee8c38a47fc9d4daf82dbdad826e4f25af21 0.0s
=> => naming to docker.io/library/multi-stage-buildkit 0.0s
❯ DOCKER_BUILDKIT=0 dkb --no-cache -f Dockerfile-multi-stage --target stage2 -t multi-stage . # 在不使用 BuildKit 的情况下构建同一目标时,所有阶段都会被处理
DEPRECATED: The legacy builder is deprecated and will be removed in a future release.
BuildKit is currently disabled; enable it by removing the DOCKER_BUILDKIT=0
environment-variable.
Sending build context to Docker daemon 10.75kB
Step 1/9 : FROM golang:1.21-alpine as base
1.21-alpine: Pulling from library/golang
96526aa774ef: Already exists
834bccaa730c: Already exists
6bde6e5b7857: Already exists
8ebe3be3b56e: Already exists
Digest: sha256:110b07af87238fbdc5f1df52b00927cf58ce3de358eeeb1854f10a8b5e5e1411
Status: Downloaded newer image for golang:1.21-alpine
---> bb520cee46ae
Step 2/9 : WORKDIR /src
---> Running in 50f10e5d4a70
Removing intermediate container 50f10e5d4a70
---> 89add9eafb7c
Step 3/9 : COPY main.go /src/main.go
---> cd7e56989a8b
Step 4/9 : RUN go build -o /bin/hello ./main.go
---> Running in dca9ddcf1099
Removing intermediate container dca9ddcf1099
---> 091a4d7d5cb7
Step 5/9 : FROM alpine:3.18 as stage1
3.18: Pulling from library/alpine
96526aa774ef: Already exists
Digest: sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978
Status: Downloaded newer image for alpine:3.18
---> 8ca4688f4f35
Step 6/9 : RUN echo "Hello, stage1"
---> Running in efe5def848eb
Hello, stage1
Removing intermediate container efe5def848eb
---> 0cfb00902193
Step 7/9 : FROM scratch as stage2
--->
Step 8/9 : COPY --from=base /bin/hello /bin/hello
---> 47ffe873ac7b
Step 9/9 : CMD ["/bin/hello"]
---> Running in a064bc18979d
Removing intermediate container a064bc18979d
---> eb7aef71c12b
Successfully built eb7aef71c12b
Successfully tagged multi-stage:latestARG(Argument)指令用于定义构建时传递给镜像的参数(在构建时通过 --build-arg 选项传递)。ARG 指令可以增加 Docker 镜像构建的灵活性,允许在构建过程中传递一些参数,从而根据需要进行定制化。# 使用 ARG 定义一个参数
ARG BASE_IMAGE=alpine:latest
# 使用 FROM 指定基础镜像,这里使用了 ARG 定义的参数
FROM ${BASE_IMAGE}
# 在构建过程中可以使用 ARG 定义的参数
ARG APP_VERSION=1.0
# 其他构建步骤
# ...
# 在构建过程中使用 ARG 定义的参数
LABEL version=${APP_VERSION}
# 其他构建步骤
# ...在 FROM ${BASE_IMAGE} 中,${BASE_IMAGE} 使用了该参数,允许在构建时通过 --build-arg 选项传递不同的基础镜像。
docker build --build-arg BASE_IMAGE=ubuntu:latest -t my-custom-image .DANGER
docker history 命令看到构建时变量的值。❯ cat Dockerfile-arg
FROM alpine:3.18 as base
CMD ["/bin/bash"]
ARG OUTPUT_STRING="Hello World"
RUN echo "${OUTPUT_STRING}"
❯ docker build --build-arg OUTPUT_STRING="hello ryan~" -t arg-test -f Dockerfile-arg .
[+] Building 1.6s (6/6) FINISHED docker:default
=> [internal] load build definition from Dockerfile-arg 0.0s
=> => transferring dockerfile: 147B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/alpine:3.18 1.1s
=> [1/2] FROM docker.io/library/alpine:3.18@sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978 0.1s
=> => resolve docker.io/library/alpine:3.18@sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978 0.0s
=> => sha256:48d9183eb12a05c99bcc0bf44a003607b8e941e1d4f41f9ad12bdcc4b5672f86 528B / 528B 0.0s
=> => sha256:8ca4688f4f356596b5ae539337c9941abc78eda10021d35cbc52659c74d9b443 1.47kB / 1.47kB 0.0s
=> => sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978 1.64kB / 1.64kB 0.0s
=> [2/2] RUN echo "hello ryan~" 0.3s
=> exporting to image 0.1s
=> => exporting layers 0.0s
=> => writing image sha256:23aa03674aec5fd41c71471700fb1b5a7f6bfa53e4a6417a2bfbe3f5382bd63f 0.0s
=> => naming to docker.io/library/arg-test 0.0s
❯ docker history arg-test
IMAGE CREATED CREATED BY SIZE COMMENT
23aa03674aec 14 seconds ago RUN |1 OUTPUT_STRING=hello ryan~ /bin/sh -c … 0B buildkit.dockerfile.v0
<missing> 14 seconds ago ARG OUTPUT_STRING=Hello World 0B buildkit.dockerfile.v0
<missing> 14 seconds ago CMD ["/bin/bash"] 0B buildkit.dockerfile.v0
<missing> 7 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 7 weeks ago /bin/sh -c #(nop) ADD file:756183bba9c7f4593… 7.34MB在 Docker 中,CMD 和 ENTRYPOINT 是两个用于定义容器启动命令的关键指令。它们可以一起使用,也可以单独使用,取决于你的需求。下面是对它们的详细解释:
CMD 指令用于定义容器启动时默认执行的命令。它有两种不同的形式:
Shell 形式:
CMD command param1 param2这种形式使用默认的 shell 执行命令。例如:
CMD echo "Hello, World!"这将在容器启动时执行 echo "Hello, World!"。
Exec 形式:
CMD ["executable","param1","param2"]这种形式直接执行指定的可执行文件,不使用默认的 shell。例如:
CMD ["/bin/bash", "-c", "echo Hello, World!"]这会以非交互的方式执行 Bash,并输出 "Hello, World!"。
CMD 指令可以被 Dockerfile 中的最后一个有效 CMD 指令所覆盖。如果用户在运行容器时指定了命令,它将替换 CMD 中的默认命令。
ENTRYPOINT 指令用于配置容器启动时执行的命令。它也有两种形式:
Shell 形式:
ENTRYPOINT command param1 param2这种形式使用默认的 shell 执行命令。
Exec 形式:
ENTRYPOINT ["executable", "param1", "param2"]这种形式直接执行指定的可执行文件,不使用默认的 shell。
ENTRYPOINT 指令的一个关键特性是,它可以与 CMD 指令结合使用。如果定义了 ENTRYPOINT,那么 CMD 的内容会被传递给 ENTRYPOINT 作为参数。这种组合使得你可以定义一个容器的主要执行命令,同时保留一些默认参数。
在 Dockerfile 中,你可以同时定义 ENTRYPOINT 和 CMD,这样它们就会形成一个命令的基础。CMD 提供默认参数,而 ENTRYPOINT 提供主要的执行命令。例如:
FROM ubuntu:latest
ENTRYPOINT ["echo", "Hello,"]
CMD ["World!"]在这个例子中,当你运行容器时,它会执行 echo Hello, World!。如果你在运行容器时指定了额外的命令,它们将替换 CMD 中的默认参数。
docker run my-image Hi there!上面的命令将输出:Hello, Hi there!,因为 "Hi there!" 替换了默认的 "World!"。
总体而言,CMD 和 ENTRYPOINT 的结合使用为容器提供了灵活性,允许用户在运行容器时指定不同的参数,同时保留了容器的主要执行命令。
ENTRYPOINT 和 CMD 都是 Dockerfile 中用于定义容器启动命令的指令,它们之间有一些关键的区别:
ENTRYPOINT
ENTRYPOINT 用于配置容器启动时执行的主要命令。它可以是一个可执行文件或者一个 shell 命令。ENTRYPOINT 定义的命令不会被 docker run 命令中指定的命令参数覆盖。它始终是容器启动时执行的主要命令。CMD
提供默认参数:CMD 用于为容器提供默认的执行参数。它可以包含可执行文件及其参数,或者是一个 shell 命令。
可以被覆盖:CMD 中定义的命令可以被 docker run 命令中指定的命令参数覆盖。如果用户在运行容器时提供了命令参数,它们将替换 CMD 中定义的默认参数。
CMD ["echo", "World!"]如果运行容器时提供额外的参数,它们将替换 CMD 中的默认参数。
docker run my-image Hi there!这会输出:Hi there!
可被覆盖或扩展:CMD 可以被 Dockerfile 中的最后一个有效 CMD 指令所覆盖,也可以被 docker run 命令中的参数扩展。
❯ cat Dockerfile-ENTRYPOINT
FROM alpine:3.18
ENTRYPOINT [ "echo", "Ryan" ]
CMD [ "666~" ]%
❯ docker build -f Dockerfile-ENTRYPOINT -t entrypoint-cmd .
[+] Building 0.9s (5/5) FINISHED docker:default
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load build definition from Dockerfile-ENTRYPOINT 0.0s
=> => transferring dockerfile: 115B 0.0s
=> [internal] load metadata for docker.io/library/alpine:3.18 0.7s
=> CACHED [1/1] FROM docker.io/library/alpine:3.18@sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:a9114a4a3719ed5cdf7b097f338e3a5be112e52644ecc42c527f0f2231530311 0.0s
=> => naming to docker.io/library/entrypoint-cmd 0.0s
❯ docker run entrypoint-cmd:latest
Ryan 666~
❯ docker run entrypoint-cmd:latest 888~
Ryan 888~COPY 和 ADD 是 Dockerfile 中用于将文件或目录从主机复制到容器中的两个指令。它们之间有一些区别,下面是详细解释:
COPY 指令用于将文件或目录从构建上下文的主机系统复制到 Docker 镜像中。语法如下:
COPY <源路径>... <目标路径>
# 将主机上当前目录下的 app 文件夹复制到容器中的 /app 目录
COPY ./app /app<源路径>:是主机上的文件或目录的路径,可以是相对路径或绝对路径。<目标路径>:是容器中的目标路径,表示文件或目录将被复制到容器的哪个位置。ADD 指令的功能与 COPY 类似,但具有更多的功能。除了复制文件,ADD 还可以自动解压缩压缩文件、从 URL 复制文件,并具有一些其他特性。语法如下:
ADD <源路径>... <目标路径>
# 将主机上当前目录下的 app 文件夹复制到容器中的 /app 目录
ADD ./app /app与 COPY 相同,<源路径> 是主机上的文件或目录的路径,而 <目标路径> 是容器中的目标路径。
COPY 和 ADD
功能差异:
COPY 专注于将本地文件系统上的文件或目录复制到容器中。ADD 不仅可以复制文件,还具有一些扩展功能,如解压缩压缩文件、从 URL 复制文件等。建议使用 COPY:
COPY,因为它更明确,功能更单一。如果只需要复制文件,COPY 是更好的选择。缓存问题:
COPY 和 ADD 指令都会使用缓存。但是,由于 ADD 具有更多功能,可能在某些情况下导致不必要的缓存失效,因此在一般情况下,COPY 更容易控制和预测。解压缩:
ADD 在复制压缩文件时会自动解压,而 COPY 不会自动解压。如果你的需求是将文件复制到容器中而不解压,可以使用 COPY。总体而言,对于普通的文件复制操作,推荐使用 COPY,而对于具有特殊需求的情况,可以考虑使用 ADD。
❯ tar -czvf demo.tar.gz demo
demo/
demo/demo1.txt
demo/demo2.txt
demo/demo4.txt
demo/demo5.txt
demo/demo3.txt
❯ cat Dockerfile-COPY-ADD
FROM alpine:3.18
# install tree
# sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.cloud.tencent.com/g' /etc/apk/repositories
RUN apk add --no-cache tree
RUN mkdir /add-dir /copy-dir
COPY demo.tar.gz /copy-dir/
ADD demo.tar.gz /add-dir/
CMD ["tree", "/add-dir", "/copy-dir"]
❯ dk build -t copy-add-demo -f Dockerfile-COPY-ADD .
[+] Building 3.1s (11/11) FINISHED docker:default
=> [internal] load build definition from Dockerfile-COPY-ADD 0.0s
=> => transferring dockerfile: 423B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/alpine:3.18 0.7s
=> CACHED [1/6] FROM docker.io/library/alpine:3.18@sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 33B 0.0s
=> [2/6] RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.cloud.tencent.com/g' /etc/apk/repositories 0.2s
=> [3/6] RUN apk add --no-cache tree 1.4s
=> [4/6] RUN mkdir /add-dir /copy-dir 0.4s
=> [5/6] COPY demo.tar.gz /copy-dir/ 0.1s
=> [6/6] ADD demo.tar.gz /add-dir/ 0.1s
=> exporting to image 0.2s
=> => exporting layers 0.2s
=> => writing image sha256:d4bdc32841fc21bd12067a8d3993c0e4ffcc129a22f105e4aebb380e59e5ceda 0.0s
=> => naming to docker.io/library/copy-add-demo 0.0s
❯ docker run --name copy-add-demo copy-add-demo:latest # 可以看到 COPY 的文件没有解压,而 ADD 的文件解压了
/add-dir
└── demo
├── demo1.txt
├── demo2.txt
├── demo3.txt
├── demo4.txt
└── demo5.txt
/copy-dir
└── demo.tar.gz
3 directories, 6 files