Container

本页介绍 MikroTik RouterOS 的容器支持,涵盖配置、虚拟接口、存储和安全注意事项。详细说明重启间隔、DNS 设置、CPU 分配等属性。

容器

本节介绍 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 也可能无法修复。

要求

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

危险

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

子菜单: /container

属性

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

警告

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

菜单特定命令

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

容器配置

子菜单: /container/config

属性说明
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;默认:)每个容器最大交换空间(字节)的全局默认值。单个容器可以用自己的 swap-max 设置覆盖此值。需要设备上启用 swap(参见交换空间)。
username指定用于认证的用户名(从 ROS 7.8 开始)
password 敏感指定用于认证的密码(从 ROS 7.8 开始)

只读属性

属性说明
assumed-registry-url自动检测到的注册表 URL。
memory-current所有容器当前的总内存使用量(字节)。

保存容器

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

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

使用 commit 参数保存会将 root-dir 层添加到 OCI 归档格式中,或更改原始镜像配置。在 podman 或 docker 中导入未使用 commit 保存的镜像时,不会显示在 ROS 容器条目中所做的更改,例如指定的环境变量、入口点、root-dir 中的文件以及任何其他更改。

使用 commit 参数可以将 root 目录层连同 ROS 容器条目中指定的附加参数一起保存(提交)到 OCI 镜像,以便导入 podman 或 docker。

命令语法

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

参数

参数说明
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

设置容器入口点、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

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

/container/save 0 file=out.tar commit

提交后的镜像会将 root-dir 内容添加为另一个层,并将入口点、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. 启用容器模式,并按照命令给出的说明操作(阅读有关 Device-mode 的更多信息)。你需要通过按下复位按钮或冷重启(如果在 x86 上使用容器)来确认 device-mode:

    /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 接口,为不同容器创建隔离网络。 :::

  3. 创建一个将用于容器的桥接,并分配与 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 应指向外部驱动器。不建议将内部存储用于容器。

    重要: 获取容器镜像有多种方式,如果你需要添加容器镜像的其他方式,请查看添加容器镜像部分。

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

  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. 你应该可以在 Web 浏览器中访问 http://192.168.88.1/admin/ 来打开 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
    
  • Webapp 容器配置:

    /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
    
  • 第一个和第二个 Webapp 容器的配置。

    /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。

第 2 层网络中的容器

在此网络设置中,容器直接连接到与其他物理网络设备相同的第 2 层网络。此网络设置相当于其他容器引擎(如 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]
    
  • Webapp 配置。

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

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

容器的 IPv4 和 IPv6

在此网络设置中,容器将能够通过 IPv4 和 IPv6 通信。该解决方案基于带 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
    
  • Webapp 容器配置。

    /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 请求就足以验证应用程序是否正常响应。

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

健康检查示例

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

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

要设置电子邮件工具,请参阅我们的电子邮件文档页面

  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. 创建容器后,我们可以通过设置健康检查命令来启用健康检查。对于此特定容器,我们可以增加 healthcheck-interval 并指定启动期,因为 HomeAssistant 是一个较重的容器,在某些设备上可能启动较慢。

    /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(参见交换空间)。

提示与技巧

  • 容器会占用大量磁盘空间。强烈建议使用 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 使用的串行控制台