Container

本页文档介绍了 MikroTik RouterOS 的容器支持,涵盖配置、虚拟接口、存储和安全注意事项。详细说明了重启间隔、DNS 设置、CPU 分配和内存限制等属性,用于在运行容器化服务时警告安全风险。

Container

本部分介绍 RouterOS 的容器支持,包括容器配置、虚拟以太网接口、存储和应用示例。借助本部分可在 RouterOS 设备上运行受支持的容器化服务。

所需软件包: container

容器是 MikroTik 对 Linux 容器的实现,允许用户在 RouterOS 中运行容器化环境。容器功能在最新的 MikroTik RouterOS v7.x 版本中可用。容器兼容来自 Docker Hub、GCR、Quay 或其他提供商的镜像,也兼容在其他设备上使用这些提供商所支持的相同格式构建的镜像。虽然 RouterOS 使用的语法与 Docker 不同,但依然实现了类似的功能。

免责声明

危险

您需要物理访问您的 RouterOS 设备才能启用容器功能的支持;默认情况下该功能处于禁用状态;

  • 一旦启用容器功能,就可以远程添加/配置/启动/停止/移除容器!
  • 如果您的 RouterOS 设备被攻陷,容器可被用于轻松在您的 RouterOS 设备及网络上安装恶意软件;
  • 您的 RouterOS 设备的安全性等同于您在容器中运行的任何东西的安全性;
  • 如果您运行容器,没有任何类型的安全保证;
  • 在您的 RouterOS 设备上运行第三方容器镜像可能会打开安全漏洞/攻击向量/攻击面;
  • 具备构建漏洞利用知识的专家将能够越狱/提权至 root;

安全风险

  • 当安全专家发布其漏洞利用研究时——任何人都可以应用此类漏洞利用。
  • 有人可以构建一个能够利用该漏洞并提供 Linux root shell 的容器镜像。
  • 通过使用 root shell,有人可能会在您的 RouterOS 系统中留下永久性后门/漏洞,即使在移除容器镜像并禁用容器功能之后也是如此。
  • 如果漏洞被注入到主或副 RouterBOOT(或供应商预加载程序)中,那么即使是 Netinstall 也可能无法修复它。

要求

Container 软件包兼容 arm、arm64 和 x86 架构。使用 remote-image(类似于 docker pull)功能需要在主内存中有大量空闲空间,16MB SPI flash 板可使用 USB 或其他磁盘介质上的预构建镜像。

危险

  • 建议使用支持至少 100MB/s 顺序读写速度和 10K 随机 iops 的外部磁盘。使用较慢的磁盘时,容器解压时间可能会变长。
  • 需要安装 Container 软件包
  • 对于搭载 EN7562CT CPU 的设备(如 hEX Refresh),仅支持 arm32v5 容器镜像,这意味着可运行的容器数量有限。

子菜单: /container

属性

PropertyDescription
restart-interval (string; Default: )指定在容器发生故障时重启容器的间隔。示例:10s
cmd (string; Default: )CMD 的主要目的是为执行中的容器提供默认值。这些默认值可以包含可执行文件,也可以省略可执行文件,此时您还必须指定 ENTRYPOINT 指令。
comment (string; Default: )简短描述
dns (string; Default: )如果容器需要不同的 DNS,可在此配置
domain-name (string; Default: )设置容器的域名
entrypoint (string; Default: )ENTRYPOINT 允许您指定在启动容器时要运行的可执行文件。示例:/bin/sh
envlist (string; Default: )要与容器一起使用的环境变量列表(在 /container/envs 下配置)
file (string; Default: )如果从文件导入容器,则为容器 *tar.gz tar 包
hostname (string; Default: )为容器分配主机名有助于更轻松地识别和管理容器
interface (string; Default: )要与容器一起使用的 veth 接口
logging (string; Default: )如果设置为 yes,所有容器生成的输出将显示在 RouterOS 日志中
start-on-boot (string; Default: )如果设置为 yes,容器将在设备启动时自动启动。
mountlists (string; Default: )来自 /container/mounts/ 子菜单的挂载,要与本容器一起使用
mount (string; Default: )指定要用作挂载的目录
remote-image (string; Default: )如果使用外部镜像仓库(在 /container/config/set registry-url=... 下配置),则为要安装的容器镜像名称
root-dir (string; Default: )用于将容器存储保存在主内存之外
stop-signal (string; Default: 15)定义如果容器在 10 秒后仍在运行,将发送哪个 Linux 信号来终止容器。不同的信号可允许优雅关闭、立即终止或触发应用特定的操作
workdir (string; Default: )cmd entrypoint 的工作目录
devices (string; Default: )将物理设备透传给容器
cpu-list (string; Default: )指定允许容器运行的 CPU 核心
user (string; Default: )设置在执行前容器进程以其运行的用户和组。
memory-high (int; Default: )特定容器的“软”RAM 使用限制(以字节为单位)。如果用量超过高水位边界,进程将被限流并置于回收压力下。
memory-max (int; Default: )每个容器的最大 RAM 使用限制(以字节为单位)(如果 memory-max 值小于容器 memory-current,容器进程将被终止),从 7.23 开始,此参数不能设置为低于 1MB,以避免容器在启动过程中过早被终止的情况
swap-max (int; Default: )容器可使用的最大交换空间量(以字节为单位)。需要在设备上启用 swap(参见 Swap space)。
stop-time (string; Default: )在发送 stop-signal 之前,容器优雅停止的时间限制。
tmpfs (string; Default: )容器的临时文件系统挂载点。格式:size@dir,例如 64M@/tmp。
layer-dir (string; Default: )用于存储容器镜像层的目录,用于容器之间的共享层存储。
shm-size (int; Default: )容器的 /dev/shm 共享内存文件系统大小(以字节为单位)。
default-dns (string; Default: )从系统 /ip/dns 配置继承的默认 DNS 服务器。
hosts (string; Default: )容器的自定义主机名到 IP 地址映射。格式:host=ip。
restart-policy (string; Default: )定义容器的重启策略:no(从不重启)、on-failure(仅在非零退出码时重启)或 always(始终重启)。
restart-max-count (int; Default: )在 restart-interval 时间段内允许的最大重启尝试次数。
stop-on-unhealthy (string; Default: )如果设置为 yes,当健康检查状态变为不健康时,容器将自动停止。

警告

默认情况下,容器使用与 /ip/dns 子菜单中配置的相同 DNS。如果此处未配置 DNS 且未指定容器的 dns 参数,容器将无法启动!

菜单特定命令

PropertyDescription
update更新容器镜像。自动从容器仓库拉取并解压,替换原始镜像。
kill终止指定的运行中容器。
restart重启指定的运行中容器。
repull重新拉取/解压容器镜像。
shell进入运行中容器的容器 shell。
run启动容器并进入其 shell。在容器运行后关闭时很有用。
save将容器镜像保存为包含 root-dir 内容和 ROS 特定参数的 tar 归档。参见 Saving a container。

容器配置

子菜单: /container/config

PropertyDescription
registry-url下载容器的外部镜像仓库 URL(默认:https://lscr.io/)
tmpdir容器解压目录
layer-dir用于存储容器镜像层的目录,用于容器之间的共享层存储
memory-high特定容器的全局“软”RAM 使用限制(以字节为单位)。如果用量超过高水位边界,进程将被限流并置于回收压力下。
memory-max每个容器的全局最大 RAM 使用限制(以字节为单位)(如果 memory-max 值小于容器 memory-current,容器进程将被终止),从 7.23 开始,此参数不能设置为低于 1MB,以避免容器在启动过程中过早被终止的情况
swap-max (int; Default: )每个容器的最大交换空间量(以字节为单位)的全局默认值。各个容器可通过自己的 swap-max 设置覆盖此值。需要在设备上启用 swap(参见 Swap space)。
username指定用于认证的 username(从 ROS 7.8 开始)
password sensitive指定用于认证的 password(从 ROS 7.8 开始)

只读属性

PropertyDescription
assumed-registry-url自动检测到的镜像仓库 URL。
memory-current所有容器当前的总内存使用量(以字节为单位)。

保存容器

/container 菜单下的 save 命令将容器镜像写为一个有效的 OCI 归档,该归档可在 podman、docker 环境或 RouterOS 中导入。

默认情况下,save 会写入所有文件系统层(包括 root-dir),但不会将其包含在 OCI 格式中。它会在 tar 包内写入一个特殊的 ros-container.json,其中包含 RouterOS 中指定的额外信息,包括 root-dir 层信息、envlists 等。ros-container.json 在导入到 ROS 容器时会被使用——在 docker 或 podman 中它会被忽略。

使用 commit 参数保存会将 root-dir 层添加到 OCI 归档格式或更改原始镜像配置。在没有 commit 的情况下导入 podman 或 docker 中保存的镜像,将不会显示在 ROS 容器条目中进行的更改,如指定的环境变量值、entrypoints、root-dir 中的文件以及任何其他更改。

包含 ROS 容器条目中指定的额外参数的 root 目录层,可使用 commit 参数保存(提交)到 OCI 镜像中,以便在 podman 或 docker 中导入。

命令语法

/container/save <container> file=<archive> [name=<name>] [commit]

参数

ParameterDescription
file保存归档的路径
name设置要保存为的镜像名称
commit将更改提交到 OCI 归档

保存并重新导入

此示例展示了如何保存带有 root-dir 内容的容器、将其移除,然后从保存的文件中重新添加。

以添加简单容器为例:

/container/add name=alpine remote-image=alpine interface=veth1 check-certificate=no

修改容器:

/container/run alpine
/ # apk add python3
...
/ # cat <<EOF > main.py
> import os
> import sys
> ...
> EOF
/ # exit

设置容器 entrypoint、cmd 等:

/container/set alpine entrypoint="python main.py" cmd="foo bar" env=FOO=1,BAR=2 envlists=foo,bar

运行修改后的容器并验证环境变量:

/container/run 0
>>> os.environ
environ({'BAR': '2', 'BAR2': 'this is bar 2', 'EGGS': 'e33s', 'FOO': '1', 'HOME': '/root', 'HOSTNAME': 'CHR', 'PATH': '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', 'SPAM': 'spam12', 'TERM': 'xterm', 'TZ': 'manual'})
>>> exit()

保存带有 root 目录内容的容器(保持 OCI 镜像不变):

/container/save 0 file=out.tar

此保存操作将 root-dir 内容连同一些 ROS 容器参数(如 env、envlists 等)一起添加到归档中。这不会改变原始镜像的配置参数。

移除容器并从保存的文件重新添加:

/container/remove 0
/container/add file=out.tar interface=veth1

运行重新导入的容器并再次验证环境变量:

/container/run 0
>>> os.environ
environ({'BAR': '2', 'BAR2': 'this is bar 2', 'EGGS': 'e33s', 'FOO': '1', 'HOME': '/root', 'HOSTNAME': 'CHR', 'PATH': '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', 'SPAM': 'spam12', 'TERM': 'xterm', 'TZ': 'manual'})
>>> exit()

将更改提交到 OCI

保存容器镜像并将更改 commit 到 OCI 镜像:

/container/save 0 file=out.tar commit

提交后的镜像将 root-dir 内容作为另一层添加,并将 entrypoint、cmd 等值设置为标准容器镜像参数。此更改后的镜像可加载到 podman 或 docker 系统中。

在 podman 中加载保存的容器:

$ podman image load < out.tar
Getting image source signatures
Copying blob 2db82ee6251c done
Copying blob b1928d1de551 done
Copying config 3dc8d32889 done
Writing manifest to image destination
Loaded image: localhost/alpine:latest

运行加载的镜像:

$ podman run -it --rm localhost/alpine:latest
>>> os.environ
environ({'BAR': '2', 'BAR2': 'this is bar 2', 'EGGS': 'e33s', 'FOO': '1', 'HOME': '/root', 'HOSTNAME': 'CHR', 'PATH': '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', 'SPAM': 'spam12', 'TERM': 'xterm', 'TZ': 'manual'})
>>> exit()

示例

运行 Pi-hole

先决条件

  1. 具备 RouterOS v7.4beta 或更高版本并已安装 Container 软件包的 RouterOS 设备 — 如何安装软件包
  2. 物理访问设备以启用容器模式 — 下文将进行说明
  3. 用于存储的已连接 HDD、SSD 或 USB 驱动器 — 使用 RouterOS 支持的文件系统格式化 — 如何格式化/管理磁盘

运行 Pi-hole 的步骤

  1. 启用 Container 模式并按照命令给出的指示操作(详细了解 Device-mode)。您需要通过按下 reset button 或冷重启(如果在 x86 上使用 Containers)来确认设备模式:

    /system/device-mode/update container=yes 
    

    :::danger Device-mode 默认限制容器的使用,在授予容器模式访问权限之前——请确保您的设备已完全安全。 :::

  2. 创建一个新的 veth 接口,并分配一个在您的网络中唯一的 IP 地址范围:

    /interface/veth/add name=veth1 address=172.17.0.2/24 gateway=172.17.0.1
    

    :::info 以下配置等同于其他容器引擎(如 Docker)中的“bridge”网络模式。也可以创建“host”等效配置。

    重要: 一个 veth 接口可用于多个容器。您可以创建多个 veth 接口,为不同的容器创建 isolated 网络。 :::

  3. 创建一个用于容器的新 bridge,并分配与 veth 接口网关相同的 IP 地址:

    /interface/bridge/add name=containers
    /ip/address/add address=172.17.0.1/24 interface=containers
    
  4. 将 veth 接口添加到您新创建的桥中:

    /interface/bridge/port/add bridge=containers interface=veth1
    
  5. 为出向流量创建 NAT:

    /ip/firewall/nat/add chain=srcnat action=masquerade src-address=172.17.0.0/24
    
  6. 为容器创建环境变量:

    /container/envs/add list=ENV_PIHOLE key=TZ value="Europe/Riga"
    /container/envs/add list=ENV_PIHOLE key=FTLCONF_webserver_api_password value="mysecurepassword"
    /container/envs/add list=ENV_PIHOLE key=DNSMASQ_USER value="root"
    
  7. 为容器创建挂载卷:

    /container/mounts/add list=MOUNT_PIHOLE_PIHOLE src=disk1/volumes/pihole/pihole dst=/etc/pihole
    /container/mounts/add list=MOUNT_PIHOLE_DNSMASQD src=disk1/volumes/pihole/dnsmasq.d dst=/etc/dnsmasq.d
    

    :::warning src= 指向 RouterOS 位置(也可以是 src=disk1/etc_pihole,例如,如果您决定将配置文件放在外部 USB 介质上),dst= 指向定义的位置(查阅容器手册/wiki/github 以了解应指向何处)。如果 src 目录在首次使用时不存在,则会被容器在 dst 位置的内容填充。 :::

    :::warning 强烈建议将任何容器卷放在连接到您的 RouterOS 设备的外部磁盘上。避免将容器卷放在内置存储上。 :::

  8. 配置使用特定的容器镜像仓库,例如使用 Docker.io:

    /container/config/set registry-url=https://registry-1.docker.io tmpdir=disk1/tmp
    
  9. 添加容器:

    /container/add remote-image=pihole/pihole interface=veth1 root-dir=disk1/images/pihole mountlists=MOUNT_PIHOLE_PIHOLE,MOUNT_PIHOLE_DNSMASQD envlist=ENV_PIHOLE name=pihole
    

    :::tip 如果您希望在 /log/print 中查看容器输出,则在创建容器时添加 logging=yes,root-dir 应指向外部驱动器。不建议将内部存储用于容器。

    重要: 您可以通过多种方式获取容器镜像,如果您需要替代方式添加容器镜像,请查看 Adding a Container image 部分。

    重要: 添加容器将开始下载或解压它,容器本身在下载/解压后不会自动启动,您需要在首次下载/解压后手动启动它。 :::

  10. 检查容器的状态,等待下载/解压完成且 status=stopped:

    /container/print
    
  11. 启动容器:

    /container/start pihole
    
  12. 为您的容器创建端口转发:

    /ip/firewall/nat
    add action=dst-nat chain=dstnat dst-address=192.168.88.1 dst-port=80 protocol=tcp to-addresses=172.17.0.2 to-ports=80
    
  13. 您应该能够通过导航到 http://192.168.88.1/admin/ 在 Web 浏览器中访问 Pi-hole Web 面板。

  14. 要在您的设备上开始使用 Pi-hole,请将它们的 DNS 配置更改为使用 192.168.88.1 作为您的 DNS 服务器。

添加容器镜像

您可以通过多种方式在 RouterOS 设备上运行容器镜像。请查看以下示例。

选项 A:从外部库获取镜像

设置 registry-url(用于从 Docker 镜像仓库下载容器)并将解压目录(tmpdir)设置为连接的 USB 介质:

/container/config/set registry-url=https://registry-1.docker.io tmpdir=disk1/tmp

拉取镜像:

/container/add remote-image=pihole/pihole interface=veth1 root-dir=disk1/images/pihole mountlists=MOUNT_PIHOLE_PIHOLE,MOUNT_PIHOLE_DNSMASQD envlist=ENV_PIHOLE name=pihole

镜像将自动拉取并解压到 root-dir。可使用以下命令检查状态:

/container/print

选项 B:从 PC 导入镜像

您可以使用运行 Docker 或 Podman 的 PC 下载所需的容器镜像并将其保存到归档中。我们推荐使用 Podman,因为使用 Podman 更容易为特定架构构建和下载容器。

  1. 根据 RouterOS 设备的架构下载所需的镜像。

    #For ARM64
    podman pull --arch=arm64 docker.io/pihole/pihole
    
    #For ARM
    podman pull --arch=arm docker.io/pihole/pihole
    
    #For AMD64
    podman pull --arch=amd64 docker.io/pihole/pihole
    
  2. 将容器镜像保存到归档。

    podman save pihole > pihole.tar
    
  3. 将归档上传到您的 RouterOS 设备,例如:

    rsync -av pihole.tar admin@192.168.88.1:/data/disk1/
    

    :::tip 您也可以使用 Winbox 上传文件! :::

  4. 使用上传的容器镜像归档文件在 RouterOS 设备上创建容器。

    /container/add file=disk1/pihole.tar interface=veth1 root-dir=disk1/pihole mountlists=MOUNT_PIHOLE_PIHOLE,MOUNT_PIHOLE_DNSMASQD envlist=ENV_PIHOLE name=pihole
    

选项 C:在 PC 上构建镜像

您可以构建自己的容器并在 RouterOS 设备上使用它们。虽然您可以使用 Docker 构建容器,但我们推荐使用 Podman,因为使用 Podman 更容易为特定架构构建容器。

  1. 获取所需容器镜像的源文件,例如使用 git。

    git clone https://github.com/pi-hole/docker-pi-hole.git
    cd docker-pi-hole
    
  2. 通过指定 Dockerfile 或 Containerfile 以及目标架构来构建容器镜像。

    #For ARM64
    podman build --platform linux/arm64 --tag pihole -f ./src/Dockerfile
    
    #For ARM
    podman build --platform linux/arm --tag pihole -f ./src/Dockerfile
    
    #For AMD64
    podman build --platform linux/amd64 --tag pihole -f ./src/Dockerfile
    
  3. 将容器镜像保存到归档。

    podman save pihole > pihole.tar
    
  4. 将归档上传到您的 RouterOS 设备,例如。

    rsync -av pihole.tar admin@192.168.88.1:/data/disk1/
    

    :::tip 您也可以使用 Winbox 上传文件! :::

  5. 使用上传的容器镜像归档文件在 RouterOS 设备上创建容器。

    /container/add file=disk1/pihole.tar interface=veth1 root-dir=disk1/pihole mountlists=MOUNT_PIHOLE_PIHOLE,MOUNT_PIHOLE_DNSMASQD envlist=ENV_PIHOLE name=pihole
    
替代方案:使用 Docker 构建容器镜像

要使用 Dockerfile 制作您自己的 docker 包——需要安装 docker 以及 buildx 或其他构建工具包。

安装后检查是否有额外的架构可用:

docker buildx ls

应返回:

NAME/NODE DRIVER/ENDPOINT STATUS  PLATFORMS
default * docker
  default default         running linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6

如果没有——安装额外的架构:

docker run --privileged --rm tonistiigi/binfmt --install all

拉取或创建包含 Dockerfile 的项目并构建、提取镜像(按需调整 --platform):

git clone https://github.com/pi-hole/docker-pi-hole.git
cd docker-pi-hole
docker buildx build  --no-cache --platform arm64 --output=type=docker -t pihole .
docker save pihole > pihole.tar

将 pihole.tar 上传到您的 RouterOS 设备。

Linux 系统上的镜像和对象可以被 pruned

从 tar 镜像创建容器

/container/add file=pihole.tar interface=veth1 mountlists=MOUNT_PIHOLE_PIHOLE,MOUNT_PIHOLE_DNSMASQD envlist=ENV_PIHOLE name=pihole

网络示例

带 NAT 的桥接

在此网络设置中,所有容器使用相同的 veth 接口并相互通信,没有任何防火墙限制,但您需要转发端口才能允许访问容器的端口。

例如,一个数据库容器需要与一个 Web 应用容器通信;Web 应用需要将端口 80 暴露给外界,但数据库容器不需要将任何端口暴露给外界。

  • 网络配置:

    /interface/veth/add name=veth1 address=172.17.0.2/24 gateway=172.17.0.1
    /interface/bridge/add name=containers
    /ip/address/add address=172.17.0.1/24 interface=containers
    /interface/bridge/port/add bridge=containers interface=veth1
    /ip/firewall/nat
    add chain=srcnat action=masquerade src-address=172.17.0.0/24
    add action=dst-nat chain=dstnat dst-address=192.168.88.1 dst-port=80 protocol=tcp to-addresses=172.17.0.2 to-ports=80
    
  • 数据库容器配置:

    /container/envs/add list=ENV_POSTGRES key=POSTGRES_DB value="webapp"
    /container/envs/add list=ENV_POSTGRES key=POSTGRES_PASSWORD value="<changeme>"
    /container/envs/add list=ENV_POSTGRES key=POSTGRES_USER value="webapp"
    /container/envs/add list=ENV_POSTGRES key=PGDATA value="/var/lib/postgresql/data/pgdata"
    /container/envs/add list=ENV_POSTGRES key=POSTGRES_INITDB_ARGS value="--encoding='UTF8' --lc-collate='C' --lc-ctype='C'"
    /container/mounts/add list=MOUNT_POSTGRES src=disk1/volumes/postgres/data dst=/var/lib/postgresql/data
    /container/add remote-image=postgres:15 interface=veth1 root-dir=disk1/images/postgres mountlists=MOUNT_POSTGRES envlist=ENV_POSTGRES name=postgres start-on-boot=yes logging=yes
    
  • Web 应用容器配置:

    /container/add remote-image=dpage/pgadmin4 interface=veth1 root-dir=disk1/images/pgadmin name=pgadmin start-on-boot=yes logging=yes
    

在此示例中,pgadmin 的端口 80 对所有人可访问,但 postgres 的端口 5432 不对所有人可访问;它只能通过 pgadmin 作为 127.0.0.1,或通过运行该容器的 RouterOS 设备作为 172.17.0.2 访问。

隔离的容器

在此网络设置中,您有多个容器,并且希望确保其中一些可以无防火墙限制地通信,但另一些需要与其它容器隔离。例如,您可能想要创建两个数据库容器并将它们隔离。

  • 网络配置。

    /interface/veth/add name=veth1 address=172.17.0.2/24 gateway=172.17.0.1
    /interface/veth/add name=veth2 address=172.18.0.2/24 gateway=172.18.0.1
    /interface/bridge/add name=containers1
    /interface/bridge/add name=containers2
    /ip/address/add address=172.17.0.1/24 interface=containers1
    /ip/address/add address=172.18.0.1/24 interface=containers2
    /interface/bridge/port/add bridge=containers1 interface=veth1
    /interface/bridge/port/add bridge=containers2 interface=veth2
    /ip/firewall/nat
    add chain=srcnat action=masquerade src-address=172.17.0.0/24
    add chain=srcnat action=masquerade src-address=172.18.0.0/24
    add action=dst-nat chain=dstnat dst-address=192.168.88.1 dst-port=81 protocol=tcp to-addresses=172.17.0.2 to-ports=80
    add action=dst-nat chain=dstnat dst-address=192.168.88.1 dst-port=82 protocol=tcp to-addresses=172.18.0.2 to-ports=80
    
  • 第一个和第二个数据库容器配置。

    /container/envs/add list=ENV_POSTGRES1 key=POSTGRES_DB value="webapp1"
    /container/envs/add list=ENV_POSTGRES1 key=POSTGRES_PASSWORD value="<changeme>"
    /container/envs/add list=ENV_POSTGRES1 key=POSTGRES_USER value="webapp1"
    /container/envs/add list=ENV_POSTGRES1 key=PGDATA value="/var/lib/postgresql/data/pgdata"
    /container/envs/add list=ENV_POSTGRES1 key=POSTGRES_INITDB_ARGS value="--encoding='UTF8' --lc-collate='C' --lc-ctype='C'"
    /container/mounts/add list=MOUNT_POSTGRES1 src=disk1/volumes/postgres1/data dst=/var/lib/postgresql/data
    /container/add remote-image=postgres:15 interface=veth1 root-dir=disk1/images/postgres1 mountlists=MOUNT_POSTGRES1 envlist=ENV_POSTGRES1 name=postgres1 start-on-boot=yes logging=yes
    
    /container/envs/add list=ENV_POSTGRES2 key=POSTGRES_DB value="webapp2"
    /container/envs/add list=ENV_POSTGRES2 key=POSTGRES_PASSWORD value="<changeme>"
    /container/envs/add list=ENV_POSTGRES2 key=POSTGRES_USER value="webapp2"
    /container/envs/add list=ENV_POSTGRES2 key=PGDATA value="/var/lib/postgresql/data/pgdata"
    /container/envs/add list=ENV_POSTGRES2 key=POSTGRES_INITDB_ARGS value="--encoding='UTF8' --lc-collate='C' --lc-ctype='C'"
    /container/mounts/add list=MOUNT_POSTGRES2 src=disk1/volumes/postgres2/data dst=/var/lib/postgresql/data
    /container/add remote-image=postgres:15 interface=veth2 root-dir=disk1/images/postgres2 mountlists=MOUNT_POSTGRES2 envlist=ENV_POSTGRES2 name=postgres2 start-on-boot=yes logging=yes
    
  • 第一个和第二个 Web 应用容器配置。

    /container/add remote-image=dpage/pgadmin4 interface=veth1 root-dir=disk1/images/pgadmin1 name=pgadmin1 start-on-boot=yes logging=yes
    
    /container/add remote-image=dpage/pgadmin4 interface=veth2 root-dir=disk1/images/pgadmin2 name=pgadmin2 start-on-boot=yes logging=yes
    

在此示例中,pgadmin1 能够访问 postgres1,但无法访问 postgres2。类似地,pgadmin2 能够访问 postgres2,但无法访问 postgres1。

位于 Layer2 网络中的容器

在此网络设置中,您的容器直接连接到与其它物理网络设备相同的 Layer2 网络。此网络设置等同于其他容器引擎(如 Docker)上的“host”网络模式。

危险

在此网络设置中,容器上的所有端口都是暴露的。这被认为是不安全的,但会略微提升容器的网络性能。

  • 网络配置。

    /interface/veth/add name=veth1 address=192.168.88.2/24 gateway=192.168.88.1
    /interface/bridge/port/add bridge=bridge interface=veth1
    
  • 如果您的 RouterOS 设备上有服务运行在相同的端口上,您需要禁用它们。

    /ip/service/disable [find where name=www]
    
  • Web 应用配置。

    /container/add remote-image=dpage/pgadmin4 interface=veth1 root-dir=disk1/images/pgadmin name=pgadmin start-on-boot=yes logging=yes
    

在此示例中,pgadmin 容器不需要端口转发,但容器正在使用的所有其它端口现在对同一 Layer2 网络上的其他设备可访问。此类设置仅应在您的应用要求容器在相同的 Layer2 网络中拥有 IP 地址时使用,例如使用广播流量进行服务发现的应用(在大多数情况下,此类需求仍可通过使用 NAT 绕过)。

容器的 IPv4 和 IPv6

在此网络设置中,您的容器将能够通过 IPv4 和 IPv6 进行通信。该方案基于 Bridge with NAT 网络设置。

  • 网络配置。

    /ip/address
    add address=172.17.0.1/24 interface=containers
    /ip/firewall/nat
    add action=masquerade chain=srcnat src-address=172.17.0.0/24
    add action=dst-nat chain=dstnat dst-address=192.168.88.1 dst-port=80 protocol=tcp to-addresses=172.17.0.2 to-ports=80
    /ipv6/address
    add address=fd8d:5ad2:24:2::1 interface=containers
    /ipv6/firewall/nat
    add action=masquerade chain=srcnat src-address=fd8d:5ad2:24:2::/64
    add action=dst-nat chain=dstnat dst-address=fd8d:5ad2:24:2::1 dst-port=80 protocol=tcp to-addresses=fd8d:5ad2:24:2::2 to-ports=80
    /interface/veth
    add address=172.17.0.2/24,fd8d:5ad2:24:2::2/64 gateway=172.17.0.1 gateway6=fd8d:5ad2:24:2::1 name=veth1
    /interface/bridge/port/add bridge=containers interface=veth1
    
  • Web 应用容器配置。

    /container/add remote-image=nginx interface=veth1 root-dir=disk1/images/nginx name=nginx start-on-boot=yes logging=yes
    

健康检查

从 RouterOS 7.23 开始,RouterOS 中已实现健康检查支持。 健康检查是一种有助于验证容器内应用是否正常运行的机制。

健康检查依赖标准退出码。用于健康检查的命令在应用健康时应返回 0,在不健康时应返回非零退出码。

在大多数情况下,对于运行 Web 界面的容器,向 localhost 发起一个简单的 HTTP GET 请求就足以验证应用是否正常响应。

PropertyDescription
healthcheck-cmd (string; Default:)用于健康检查的命令
healthcheck-interval (time; Default: 00:00:30)时间间隔,格式为 HH:MM:SS
healthcheck-retries (int; Default: 3)在检查失败的情况下——在容器应用被视为不健康之前重试的次数
healthcheck-start-interval (time; Default: 00:00:05)起始周期内健康检查之间的时间
healthcheck-start-period (time; Default: 00:00:00)为容器提供启动时间。起始周期内的任何失败都不会计入重试计数器
healthcheck-status (string)容器的当前健康状态。显示容器内应用的当前状态以及探测的输出
healthcheck-timeout (time; Default: 00:00:30)如果健康检查探测耗时超过指定周期,该检查将计为失败

健康检查示例

对于此示例,我们将设置一个 HomeAssistant 容器,并在其上配置一个简单的健康检查。

有关设置 HomeAssistant 容器的指南,请参阅我们的 HomeAssistant setup guide

要设置 E-mail 工具,请参阅我们的 E-mail documentation page

  1. 检查您的容器名称

    [admin@ROSE] > container print 
    Flags: S - STOPPED, R - RUNNING
    Columns: NAME, ROOT-DIR, INTERFACE, MEMORY-CURRENT, CPU-USAGE, CONTAINER-SIZE, DATA-SIZE, TAG
    #   NAME                ROOT-DIR                                        INTERFACE                MEMORY-CURRENT  CPU-USAGE  CONTAINER-SIZE  DATA-SIZE  TAG                                                                   
    1 S home-assistant  /nvme1/apps/home-assistant/home-assistant_root  veth-home-assistant                          0  2201.9MiB       471.9KiB   docker.io/homeassistant/home-assistant:latest
    
  2. 创建容器后,我们可以通过设置健康检查命令在其上启用健康检查。对于这个特定的容器,由于 HomeAssistant 是一个较重的容器,在某些设备上启动可能较慢,我们可以增加 healthcheck-interval 并指定一个 start-period。

    /container/set home-assistant healthcheck-cmd="curl -f http://localhost:8123" healthcheck-interval="00:01:00" healthcheck-start-period="00:02:00"
    

    如果成功且容器启动——您应该会看到容器以“Healthy”状态运行。 此外,healthcheck-status 字段会填充容器的当前状态以及来自 curl 命令的输出。

    [admin@ROSE] > container print proplist=name,healthcheck-status
    Flags: S - STOPPED, R - RUNNING, H - HEALTHY
    Columns: NAME, HEALTHCHECK-STATUS
    #   NAME                HEALTHCHECK-STATUS                                                                           
    
     H home-assistant  good, output:   % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                                              Dload  Upload   Total   Spent    Left  Speed     
    

作为额外步骤,您可以创建一个脚本,轮询容器状态并在健康检查失败时向您的电子邮件发送通知,并添加一个调度器来定期运行该脚本。

  1. 创建脚本:

    /system script
    add name=unhealthy-container-alert source="
    :global lastState\
        \n:local currentState \"\"\
        \n:local msg \"\"\
        \n:foreach i in=[/container find where unhealthy] do={\
        \n:local name [/container get \$i name]\
        \n:set currentState (\$currentState . \$name . \";\")\
        \n}\
        \n:if ([:len \$currentState] = 0) do={\
        \n:set lastState \"\"\
        \n} else={\
        \n:if (\$currentState != \$lastState) do={\
        \n:set lastState \$currentState\
        \n:foreach i in=[/container find where unhealthy] do={\
        \n:local name [/container get \$i name]\
        \n:set msg (\$msg . \$name . \" is UNHEALTHY\\n\")\
        \n}\
        \n/tool e-mail send to=\"admin@example.com\" subject=\"Unhealthy container(s)\" body=\$msg\
        \n}\
        \n}"
    

    :::warning 请确保将 admin@example.com 替换为您的电子邮件。 :::

  2. 创建调度器:

    /system scheduler
    add interval=2m name=container-healthcheck-alert on-event="/system/script/run unhealthy-container-alert"
    

内存

有三个参数控制容器内存行为:memory-max、memory-high 和 swap-max。每个参数可以按容器配置,也可以在 /container/config 中全局配置。全局值充当默认值——如果同时设置了按容器和全局限制,则应用较低的值(两种方式都适用)。

memory-max

设置容器 RAM 使用量的硬上限(以字节为单位)。如果容器超过此限制,它将被终止。当在两个层级都设置时,例如按容器限制为 500MB,全局限制为 300MB,则 300MB 限制生效。如果启用了 swap,容器可能会尝试将多余内存换出,而不是被终止。

memory-high

设置容器 RAM 使用量的软上限(以字节为单位)。当达到限制时,容器进程将被限流并置于严重的内存回收压力下——系统将尝试释放内存(包括交换)。与 memory-max 不同,容器永远不会因为超过此限制而被终止。

swap-max

定义容器可使用的最大交换空间量(以字节为单位)。与 memory-max 协同工作——一旦容器达到其 memory-max 限制,多余的内存将被换出,直至达到 swap-max 限制。例如,如果容器的 memory-max 为 600MB,而某个进程试图使用 800MB,则剩余的 200MB 将被推入 swap。设备上必须启用 swap(参见 Swap space)。

提示与技巧

  • 容器会占用大量磁盘空间。强烈推荐使用 USB/SATA、NVMe 连接的介质。对于带有 USB 端口的设备——USB 转 SATA 适配器可与 2.5" 驱动器一起使用——以获得额外存储和更快的文件操作。

  • 要使容器在路由器重启后启动,请使用 start-on-boot 选项(从 7.6beta6 开始):

    /container/print
      0 name="2e679415-2edd-4300-8fab-a779ec267058" tag="test_arm64:latest" os="linux" arch="arm" interface=veth2 
        root-dir=disk1/alpine mountlists="" dns="" logging=yes start-on-boot=yes status=running 
    
    /container/set 0 start-on-boot=yes
    
  • 可以进入运行中的容器 shell:

    /container/shell 0
    
  • 可以设置容器超时(以秒为单位),否则它将无限期保持打开:

    /container/shell 0 timeout=120
    
  • 启用日志以获取容器输出:

    /container/set 0 logging=yes
    
  • 某些容器需要额外的特权才能正常运行:

    /container/set 0 user=0:0
    
  • 您可以在容器内以特定用户执行命令,而无需调用 /bin/sh:

    /container/shell nextcloud user=www-data cmd="php /var/www/html/cron.php" no-sh
    
  • 从 7.11beta5 版本开始,可以添加多个地址和 IPv6 地址:

    /interface/veth/add address=172.17.0.3/16,fd8d:5ad2:24:2::2/64 gateway=172.17.0.1 gateway6=fd8d:5ad2:24:2::1
    
  • 主动运行的 /dev/ 节点到容器:

    /dev/full	/dev/null	/dev/random		/dev/tty	/dev/urandom	/dev/zero	/dev/console	/dev/net/tun	/dev/kvm	/dev/fuse
    
  • 使用 Devices 参数,可以允许将串行终端暴露给容器。

/container/set your-container devices=serial0:""

信息

要允许容器使用串行控制台端口,您必须先在 /system/console 下释放 RouterOS 使用的串行控制台。