# 7.5 ENTRYPOINT 入口点

## 何时使用 ENTRYPOINT：从“容器”到“命令”

如果说 CMD 是“容器中的默认程序”，那么 ENTRYPOINT 就是“把容器变成一个命令”。这个思维转变决定了你何时使用 ENTRYPOINT。

**使用 ENTRYPOINT 的典型场景**：

1. **命令行工具**：你想让镜像像 `curl` 或 `wget` 一样使用

   ```dockerfile
   ENTRYPOINT ["curl"]
   # docker run myimage http://example.com → curl http://example.com
   ```
2. **应用启动脚本**：你有一个初始化脚本，需要接收命令行参数

   ```dockerfile
   ENTRYPOINT ["/app/entrypoint.sh"]
   # docker run myimage --debug → /app/entrypoint.sh --debug
   ```
3. **与 CMD 结合**：ENTRYPOINT 定义入口，CMD 定义默认参数

   ```dockerfile
   ENTRYPOINT ["python", "app.py"]
   CMD ["--port", "8000"]
   # docker run myimage → python app.py --port 8000
   # docker run myimage --port 9000 → python app.py --port 9000
   ```

**对比 CMD**：如果没有这些“把容器当命令用”的需求，通常使用 CMD 就足够了。

## 7.5.1 什么是 ENTRYPOINT

`ENTRYPOINT` 指定容器启动时运行的入口程序。与 CMD 不同，ENTRYPOINT 定义的命令不会被 `docker run` 的参数覆盖，而是 **接收这些参数**。

> **核心作用**：让镜像像一个可执行程序一样使用，`docker run` 的参数作为这个程序的参数。

***

## 7.5.2 语法格式

| 格式           | 语法                            | 推荐程度    |
| ------------ | ----------------------------- | ------- |
| **exec 格式**  | `ENTRYPOINT [“可执行文件”, “参数1”]` | ✅**推荐** |
| **shell 格式** | `ENTRYPOINT 命令 参数`            | ⚠️ 不推荐  |

```docker
## exec 格式（推荐）

ENTRYPOINT ["nginx", "-g", "daemon off;"]

## shell 格式（不推荐）

ENTRYPOINT nginx -g "daemon off;"
```

***

## 7.5.3 ENTRYPOINT vs CMD

### 核心区别

| 特性                | ENTRYPOINT     | CMD    |
| ----------------- | -------------- | ------ |
| **定位**            | 固定的入口程序        | 默认参数   |
| **docker run 参数** | 追加为参数          | 完全覆盖   |
| **覆盖方式**          | `--entrypoint` | 直接指定命令 |
| **适用场景**          | 把镜像当命令用        | 提供默认行为 |

### 行为对比

```docker
## 只用 CMD

CMD ["curl", "-s", "http://example.com"]
```

```bash
$ docker run myimage              # curl -s http://example.com
$ docker run myimage -v           # 执行 -v（错误！）
$ docker run myimage curl -v ...  # curl -v ...（完全替换）
```

```docker
## 只用 ENTRYPOINT

ENTRYPOINT ["curl", "-s"]
```

```bash
$ docker run myimage                      # curl -s（缺参数）
$ docker run myimage http://example.com   # curl -s http://example.com ✓
```

```docker
## ENTRYPOINT + CMD 组合（推荐）

ENTRYPOINT ["curl", "-s"]
CMD ["http://example.com"]
```

```bash
$ docker run myimage                      # curl -s http://example.com（默认）
$ docker run myimage http://other.com     # curl -s http://other.com ✓
$ docker run myimage -v http://other.com  # curl -s -v http://other.com ✓
```

***

## 7.5.4 场景一：让镜像像命令一样使用

### 需求：启动前准备

创建一个查询公网 IP 的 “命令” 镜像。

### 使用 CMD 的问题

```docker
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
CMD ["curl", "-s", "http://myip.ipip.net"]
```

```bash
$ docker run myip           # ✓ 正常工作
当前 IP：61.148.226.66

$ docker run myip -i        # ✗ 错误！
exec: "-i": executable file not found

## -i 替换了整个 CMD，被当作可执行文件

...
```

### 使用 ENTRYPOINT 解决

```docker
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["curl", "-s", "http://myip.ipip.net"]
```

```bash
$ docker run myip           # ✓ 正常工作
当前 IP：61.148.226.66

$ docker run myip -i        # ✓ 添加 -i 参数
HTTP/1.1 200 OK
...
当前 IP：61.148.226.66
```

### 交互图示

```bash
ENTRYPOINT ["curl", "-s", "http://myip.ipip.net"]
            │
docker run myip -i
            │
            ▼
curl -s http://myip.ipip.net -i
└─────────────────────────────┘
     ENTRYPOINT + docker run 参数
```

***

## 7.5.5 场景二：启动前的准备工作

### 需求

在启动主服务前执行初始化脚本 (如数据库迁移、权限设置)。

### 实现方式

```docker
FROM redis:7-alpine
COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["redis-server"]
```

**docker-entrypoint.sh**：

```bash
#!/bin/sh
set -e

## 准备工作

echo "Initializing..."

## 如果第一个参数是 redis-server，以 redis 用户运行

if [ "$1" = 'redis-server' ]; then
    chown -R redis:redis /data
    exec gosu redis "$@"
fi

## 其他命令直接执行

exec "$@"
```

### 工作流程

```bash
docker run redis                    docker run redis bash
        │                                    │
        ▼                                    ▼
docker-entrypoint.sh redis-server   docker-entrypoint.sh bash
        │                                    │
        ├─ 初始化                            ├─ 初始化
        ├─ chown -R redis:redis /data        │
        └─ exec gosu redis redis-server      └─ exec bash
           (以 redis 用户运行)                  (以 root 用户运行)
```

### 关键点

1. **exec “$@”**：用传入的参数替换当前进程，确保信号正确传递
2. **条件判断**：根据 CMD 不同执行不同逻辑
3. **用户切换**：使用 `gosu` 切换用户 (比 `su` 更适合容器)

***

## 7.5.6 场景三：带参数的应用

```docker
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt

ENTRYPOINT ["python", "app.py"]
CMD ["--host", "0.0.0.0", "--port", "8080"]
```

```bash
## 使用默认参数

$ docker run myapp

## 执行: python app.py --host 0.0.0.0 --port 8080

## 覆盖参数

$ docker run myapp --host 0.0.0.0 --port 9000

## 执行: python app.py --host 0.0.0.0 --port 9000

## 完全不同的参数

$ docker run myapp --help

## 执行: python app.py --help

...
```

***

## 7.5.7 覆盖 ENTRYPOINT

使用 `--entrypoint` 参数覆盖：

```bash
## 正常运行

$ docker run myimage

## 覆盖 ENTRYPOINT 进入 shell 调试

$ docker run --entrypoint /bin/sh myimage

## 覆盖 ENTRYPOINT 并传入参数

$ docker run --entrypoint /bin/cat myimage /etc/os-release
```

***

## 7.5.8 ENTRYPOINT 与 CMD 组合表

| ENTRYPOINT      | CMD             | 最终执行命令                         |
| --------------- | --------------- | ------------------------------ |
| 无               | 无               | 无 (容器无法启动)                     |
| 无               | `["cmd", "p1"]` | `cmd p1`                       |
| `["ep", "p1"]`  | 无               | `ep p1`                        |
| `["ep", "p1"]`  | `["cmd", "p2"]` | `ep p1 cmd p2`                 |
| `ep p1` (shell) | `["cmd", "p2"]` | `/bin/sh -c "ep p1"` (CMD 被忽略) |

> ⚠️ **注意**：shell 格式的 ENTRYPOINT 会忽略 CMD！

***

## 7.5.9 最佳实践

### 1. 使用 exec 格式

```docker
## ✅ 推荐

ENTRYPOINT ["python", "app.py"]

## ❌ 避免 shell 格式

ENTRYPOINT python app.py
```

### 2. 提供有意义的默认参数

```docker
ENTRYPOINT ["nginx"]
CMD ["-g", "daemon off;"]
```

### 3. 入口脚本使用 exec

```bash
#!/bin/sh

## 准备工作...

## 使用 exec 替换当前进程

exec "$@"
```

### 4. 处理信号

确保 ENTRYPOINT 脚本能正确传递信号：

```bash
#!/bin/bash
trap 'kill -TERM $PID' TERM INT

## 启动应用

app "$@" &
PID=$!

## 等待应用退出

wait $PID
```

***
