У меня есть локальная сеть 192.168.0.0/24. В этой сети работает хост с двумя сетевыми картами, одна из которых (а) enp6s0 имеет адрес 192.168.0.162/24, — через эту катру осуществляется связь с internet и настройка по SSH. Другая сетевая карта (б) enp7s0 пока не задействована. На этом хосте (192.168.0.162/24) и (незадействованная сетевая катра) работает docker. В этом докере надо создать образ для создания контейнера с OpenWRT, чтобы у него было два интерфейса, один на сеть 192.168.0.0/24 (WAN для Openwrt) с статическим адресом 192.168.0.200, другой на незадействованную сетевую карту хоста (LAN для Openwrt) c статическим адресом 192.168.200.1/24. В контейнере с OpenWRT надо Предоставь мне Dockerfile и docker-compose.yml. При этом у меня должна быть возможность доступа к web интерфейсу Openwrt как по LAN так и по WAN интерфейсу.
Ниже — рабочий вариант на базе двух macvlan-сетей Docker:
- WAN OpenWRT: интерфейс контейнера подключается к физической сети
enp6s0, статический IP192.168.0.200/24. - LAN OpenWRT: интерфейс контейнера подключается к неиспользуемой физической карте
enp7s0, статический IP192.168.200.1/24. - LuCI/Web будет доступна:
- из LAN:
http://192.168.200.1иhttps://192.168.200.1 - из WAN-сети
192.168.0.0/24:http://192.168.0.200иhttps://192.168.0.200
Дальше предполагаю:
- ваш роутер/шлюз в сети
192.168.0.0/24имеет адрес192.168.0.1; - адрес
192.168.0.200свободен и не будет выдан DHCP-сервером; enp7s0физически подключена к отдельному LAN-сегменту: коммутатор, точка доступа, другой PC и т.п.;- используется Docker Compose v2 и современный Docker с BuildKit.
1. Подготовка хоста
Перед запуском желательно поднять enp7s0 и убедиться, что на ней нет адреса и DHCP-клиента хоста.
sudo ip link set enp6s0 up
sudo ip link set enp7s0 up
# Если на enp7s0 вдруг есть адрес, уберите его.
# Не трогайте enp6s0, через неё у вас интернет/SSH.
sudo ip addr flush dev enp7s0
Опционально, иногда полезно для macvlan:
sudo ip link set enp6s0 promisc on
sudo ip link set enp7s0 promisc on
Если у вас NetworkManager/systemd-networkd управляет enp7s0, лучше перевести её в unmanaged или убрать с неё DHCP/статический адрес. Иначе хост может мешать LAN-сегменту OpenWRT.
2. Dockerfile
Создайте отдельную директорию, например:
mkdir -p ~/openwrt-docker
cd ~/openwrt-docker
Сохраните файл Dockerfile:
# syntax=docker/dockerfile:1.8
ARG OPENWRT_TAG=23.05.4
FROM openwrt/rootfs:${OPENWRT_TAG}
ARG ROOT_PASSWORD=ChangeMe_Strong
ENV TZ=UTC
RUN set -eux; \
opkg update; \
opkg install --force-overwrite --force-space \
luci-ssl \
uhttpd \
dnsmasq \
firewall4 \
dropbear \
ip-full \
ca-bundle \
openssl-util; \
rm -rf /var/opkg-lists; \
echo "root:${ROOT_PASSWORD}" | chpasswd; \
for s in boot network firewall dnsmasq dropbear uhttpd rpcd sysctl urandom_system; do \
if [ -x "/etc/init.d/$s" ]; then "/etc/init.d/$s" enable || true; fi; \
done
COPY <<'EOF' /entrypoint.sh
#!/bin/sh
set -e
: "${HOSTNAME:=openwrt}"
: "${WAN_MAC:=02:42:c0:a8:00:c8}"
: "${LAN_MAC:=02:42:c0:a8:c8:01}"
: "${WAN_IP:=192.168.0.200}"
: "${WAN_NETMASK:=255.255.255.0}"
: "${WAN_GATEWAY:=192.168.0.1}"
: "${WAN_DNS:=192.168.0.1}"
: "${LAN_IP:=192.168.200.1}"
: "${LAN_NETMASK:=255.255.255.0}"
: "${WAN_WEB_ALLOW:=192.168.0.0/24}"
# Позволяет задать пароль при первом старте через переменную окружения ROOT_PASSWORD.
# Если пароль уже меняли вручную, файл-маркер не даст перезаписать его при каждом рестарте.
if [ -n "${ROOT_PASSWORD:-}" ] && [ ! -f /etc/.docker_root_password_set ]; then
echo "root:${ROOT_PASSWORD}" | chpasswd
touch /etc/.docker_root_password_set
fi
hostname "${HOSTNAME}" >/dev/null 2>&1 || true
non_lo_ifs() {
ip -o link show 2>/dev/null | awk -F': ' '{print $2}' | cut -d'@' -f1 | grep -E '^eth[0-9]+$' || true
}
# Ждём, пока Docker создаст оба интерфейса.
i=0
while [ "$i" -lt 15 ]; do
cnt=$(non_lo_ifs | wc -l)
[ "$cnt" -ge 2 ] && break
sleep 1
i=$((i+1))
done
find_if_by_mac() {
want=$(echo "$1" | tr 'A-F' 'a-f')
for p in /sys/class/net/*; do
n="${p##*/}"
[ "$n" = "lo" ] && continue
mac=$(cat "$p/address" 2>/dev/null || true)
mac=$(echo "$mac" | tr 'A-F' 'a-f')
if [ -n "$mac" ] && [ "$mac" = "$want" ]; then
echo "$n"
return 0
fi
done
return 1
}
WAN_IF=$(find_if_by_mac "$WAN_MAC" || true)
LAN_IF=$(find_if_by_mac "$LAN_MAC" || true)
# Если MAC-адреса не определились, пытаемся определить по порядку интерфейсов.
if [ -z "$WAN_IF" ] || [ -z "$LAN_IF" ] || [ "$WAN_IF" = "$LAN_IF" ]; then
set -- $(non_lo_ifs)
if [ -z "$WAN_IF" ]; then
WAN_IF="$1"
fi
if [ -z "$LAN_IF" ] || [ "$LAN_IF" = "$WAN_IF" ]; then
shift 2>/dev/null || true
LAN_IF="$1"
fi
fi
: "${WAN_IF:=eth0}"
: "${LAN_IF:=eth1}"
if [ "$WAN_IF" = "$LAN_IF" ]; then
LAN_IF="eth1"
fi
echo "OpenWRT container: WAN=${WAN_IF}, LAN=${LAN_IF}"
cat > /etc/config/network <<NETWORK
config interface 'loopback'
option device 'lo'
option proto 'static'
option ipaddr '127.0.0.1'
option netmask '255.0.0.0'
config interface 'wan'
option device '${WAN_IF}'
option proto 'static'
option ipaddr '${WAN_IP}'
option netmask '${WAN_NETMASK}'
option gateway '${WAN_GATEWAY}'
list dns '${WAN_DNS}'
option defaultroute '1'
config interface 'lan'
option device '${LAN_IF}'
option proto 'static'
option ipaddr '${LAN_IP}'
option netmask '${LAN_NETMASK}'
NETWORK
# Удаляем default route, который мог добавить Docker.
# OpenWRT сам создаст нужный default route через WAN_GATEWAY.
while ip route del default 2>/dev/null; do :; done
if [ -f /etc/config/system ]; then
(
set +e
uci -q set system.@system[0].hostname="${HOSTNAME}"
uci commit system
)
fi
if [ -f /etc/config/dhcp ]; then
(
set +e
uci -q set dhcp.lan=dhcp
uci set dhcp.lan.interface=lan
uci set dhcp.lan.start=100
uci set dhcp.lan.limit=150
uci set dhcp.lan.leasetime=12h
uci -q set dhcp.wan=dhcp
uci set dhcp.wan.interface=wan
uci set dhcp.wan.ignore=1
uci commit dhcp
)
fi
ensure_web_rule() {
name="$1"
port="$2"
if ! uci -q get "firewall.${name}" >/dev/null 2>&1; then
uci set "firewall.${name}=rule"
fi
uci set "firewall.${name}.name=${name}"
uci set "firewall.${name}.src=wan"
uci set "firewall.${name}.proto=tcp"
uci set "firewall.${name}.dest_port=${port}"
uci set "firewall.${name}.target=ACCEPT"
if [ -n "${WAN_WEB_ALLOW}" ]; then
uci set "firewall.${name}.src_ip=${WAN_WEB_ALLOW}"
else
uci -q delete "firewall.${name}.src_ip"
fi
}
if [ -f /etc/config/firewall ]; then
(
set +e
ensure_web_rule allow_http_wan 80
ensure_web_rule allow_https_wan 443
uci commit firewall
)
fi
# Самоподписанный сертификат для HTTPS/LuCI.
if [ ! -s /etc/uhttpd.crt ] || [ ! -s /etc/uhttpd.key ]; then
if command -v openssl >/dev/null 2>&1; then
openssl req -x509 -newkey rsa:2048 -nodes -days 825 \
-keyout /etc/uhttpd.key -out /etc/uhttpd.crt \
-subj "/CN=${HOSTNAME}" >/dev/null 2>&1 || true
chmod 600 /etc/uhttpd.key 2>/dev/null || true
fi
fi
exec /sbin/init
EOF
RUN chmod +x /entrypoint.sh
EXPOSE 80 443
ENTRYPOINT ["/entrypoint.sh"]
Важно:
ChangeMe_Strongзамените на свой пароль root для LuCI/SSH.- Если ваш шлюз не
192.168.0.1, потом нужно будет поменять это и вDockerfile/docker-compose.yml, и в секции сетей.
3. docker-compose.yml
В той же директории создайте docker-compose.yml:
services:
openwrt:
build:
context: .
args:
OPENWRT_TAG: "23.05.4"
ROOT_PASSWORD: "ChangeMe_Strong"
image: openwrt-macvlan:23.05
container_name: openwrt
hostname: openwrt
restart: unless-stopped
stop_grace_period: 30s
# Для OpenWRT в контейнере проще всего использовать privileged.
# Без этого обычно приходится долго подбирать capabilities, устройства и права.
privileged: true
environment:
TZ: "UTC"
HOSTNAME: "openwrt"
# MAC-адреса используются entrypoint-скриптом, чтобы правильно назначить eth0/eth1.
WAN_MAC: "02:42:c0:a8:00:c8"
LAN_MAC: "02:42:c0:a8:c8:01"
# Сетевые настройки OpenWRT.
WAN_IP: "192.168.0.200"
WAN_NETMASK: "255.255.255.0"
WAN_GATEWAY: "192.168.0.1"
WAN_DNS: "192.168.0.1"
LAN_IP: "192.168.200.1"
LAN_NETMASK: "255.255.255.0"
# Разрешаем доступ к web-интерфейсу со стороны WAN-сети.
# Для большей безопасности оставляем только 192.168.0.0/24.
# Если нужно разрешить любой источник, поставьте пустую строку:
# WAN_WEB_ALLOW: ""
WAN_WEB_ALLOW: "192.168.0.0/24"
volumes:
# Нужны модули ядра хоста для nftables/conntrack/NAT.
- /lib/modules:/lib/modules:ro
# Сохраняем конфигурацию OpenWRT между пересозданиями контейнера.
- openwrt-etc:/etc
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.rp_filter=0
- net.ipv4.conf.default.rp_filter=0
- net.ipv4.conf.all.send_redirects=0
- net.ipv4.conf.default.send_redirects=0
- net.ipv4.conf.all.accept_redirects=0
- net.ipv4.conf.default.accept_redirects=0
networks:
wan:
ipv4_address: 192.168.0.200
mac_address: "02:42:c0:a8:00:c8"
priority: 1000
lan:
ipv4_address: 192.168.200.1
mac_address: "02:42:c0:a8:c8:01"
priority: 900
networks:
wan:
driver: macvlan
driver_opts:
parent: enp6s0
enable_ipv6: false
ipam:
config:
- subnet: 192.168.0.0/24
gateway: 192.168.0.1
# Разрешаем Docker выделять только адрес контейнера.
# Если compose ругается на ip_range, можно удалить эту строку.
ip_range: 192.168.0.200/32
lan:
driver: macvlan
driver_opts:
parent: enp7s0
enable_ipv6: false
ipam:
config:
- subnet: 192.168.200.0/24
# 192.168.200.254 здесь -- служебный placeholder-шлюз для IPAM Docker.
# Сам OpenWRT будет 192.168.200.1.
# Не используйте 192.168.200.254 для других устройств.
gateway: 192.168.200.254
# Разрешаем Docker выделять только адрес контейнера.
# Если compose ругается на ip_range, можно удалить эту строку.
ip_range: 192.168.200.1/32
volumes:
openwrt-etc:
Если ваш старый Docker Compose ругается на параметры mac_address или priority внутри services.openwrt.networks.*, удалите эти строки. Тогда важно сохранить порядок сетей: сначала wan, потом lan. В entrypoint есть fallback по порядку интерфейсов.
4. Сборка и запуск
Находясь в директории с Dockerfile и docker-compose.yml:
docker compose build --pull
docker compose up -d
Смотрим лог:
docker compose logs -f openwrt
В логе примерно должна быть строка:
OpenWRT container: WAN=eth0, LAN=eth1
Проверить интерфейсы внутри контейнера:
docker exec -it openwrt ip -br addr
Проверить WAN-шлюз:
docker exec -it openwrt ping -c 3 192.168.0.1
Проверить интернет через NAT OpenWRT:
docker exec -it openwrt ping -c 3 8.8.8.8
Или:
docker exec -it openwrt ping -c 3 1.1.1.1
5. Доступ к web-интерфейсу OpenWRT
После запуска:
Из LAN-сегмента OpenWRT
Подключите устройство к сети, которая физически висит на enp7s0.
Устройства должны получать DHCP из диапазона 192.168.200.100—192.168.200.249.
Откройте:
http://192.168.200.1
https://192.168.200.1
Из вашей основной сети 192.168.0.0/24
С любого устройства в сети 192.168.0.0/24 откройте:
http://192.168.0.200
https://192.168.0.200
HTTPS будет использовать самоподписанный сертификат, поэтому браузер покажет предупреждение. Это нормально. Если хотите, потом можно импортировать сертификат или выпустить свой.
Логин:
root
Пароль:
ChangeMe_Strong
Или тот, который вы указали в Dockerfile в ARG ROOT_PASSWORD и в docker-compose.yml в build.args.ROOT_PASSWORD.
Обязательно смените пароль в LuCI:
System -> Administration
6. Важный момент: доступ к контейнеру с самого Docker-хоста
Из-за особенности macvlan Docker-хост часто не может напрямую обращаться к контейнеру в той же подсети через родительский интерфейс.
То есть с других машин в сети 192.168.0.0/24 и 192.168.200.0/24 доступ должен быть, а вот сам хост 192.168.0.162 может не открывать 192.168.0.200 без дополнительного macvlan-интерфейса.
Если вам нужен доступ именно с самого хоста, создайте служебные macvlan-интерфейсы.
Для WAN-доступа к OpenWRT:
sudo ip link add link enp6s0 name host-wan type macvlan mode bridge
sudo ip addr add 192.168.0.201/32 dev host-wan
sudo ip link set host-wan up
sudo ip route add 192.168.0.200/32 dev host-wan
Для LAN-доступа к OpenWRT:
sudo ip link add link enp7s0 name host-lan type macvlan mode bridge
sudo ip addr add 192.168.200.253/32 dev host-lan
sudo ip link set host-lan up
sudo ip route add 192.168.200.1/32 dev host-lan
После этого с хоста должны открываться:
http://192.168.0.200
http://192.168.200.1
Эти команды не сохраняются после перезагрузки автоматически. Если нужно, их можно оформить через systemd-networkd, rc.local, NetworkManager dispatcher или другой механизм в вашем дистрибутиве.
7. Что делать, если адреса или интерфейс поменялись
Если вы хотите изменить адреса, меняйте их одновременно в нескольких местах.
Например, для WAN:
services.openwrt.environment.WAN_IPservices.openwrt.networks.wan.ipv4_addressnetworks.wan.ipam.config[0].subnetnetworks.wan.ipam.config[0].gateway, если меняется шлюз- при необходимости
WAN_GATEWAY,WAN_DNS,WAN_WEB_ALLOW
Для LAN:
services.openwrt.environment.LAN_IPservices.openwrt.networks.lan.ipv4_addressnetworks.lan.ipam.config[0].subnet
Потом:
docker compose down
docker compose up -d
8. Если нужно полностью сбросить конфигурацию OpenWRT
Конфигурация хранится в Docker volume openwrt-etc. Чтобы сбросить её к состоянию из образа:
docker compose down
docker volume ls
docker volume rm <имя_проекта>_openwrt-etc
docker compose up -d
Имя проекта обычно совпадает с именем директории. Например, если папка называется openwrt-docker, volume может называться:
openwrt-docker_openwrt-etc
9. Важные замечания по безопасности
- LuCI на WAN-интерфейсе — потенциально опасно, если WAN когда-нибудь станет настоящим интернетом.
В конфигурации выше доступ к web-интерфейсу со стороны WAN ограничен источником:
192.168.0.0/24
Это переменная окружения:
WAN_WEB_ALLOW: "192.168.0.0/24"
Если вам нужен доступ только с конкретного адреса, лучше указать, например:
WAN_WEB_ALLOW: "192.168.0.162/32"
- SSH из WAN по умолчанию не разрешён. Разрешён только web 80/443. Это сделано специально. Если вам нужен SSH со стороны WAN, лучше не открывать его на весь WAN, а использовать VPN или отдельное правило с ограничением источника.
- Пароль root обязательно нужно сменить.
- Если
enp7s0иenp6s0физически подключены к одному и тому же коммутатору без VLAN, LAN-сегмент OpenWRT может оказаться в одном L2-домене с основной сетью. Это нежелательно. Лучше использовать отдельный коммутатор/VLAN дляenp7s0.
10. Короткая проверка работы
После запуска:
docker exec -it openwrt ip -br addr
Ожидается примерно:
lo UNKNOWN 127.0.0.1/8
eth0 UP 192.168.0.200/24
eth1 UP 192.168.200.1/24
Маршруты:
docker exec -it openwrt ip route
Должен быть default route через ваш шлюз 192.168.0.1.
Firewall-правила для WAN-web:
docker exec -it openwrt uci show firewall | grep allow_http
docker exec -it openwrt uci show firewall | grep allow_https
Если всё работает, открывайте браузером:
http://192.168.200.1
http://192.168.0.200
root@panel:/opt/openwrt# docker compose build —pull
[+] Building 6.8s (4/4) FINISHED docker:default
=> [openwrt internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 5.48kB 0.0s
=> [openwrt] resolve image config for docker-image://docker.io/docker/dockerfile:1.8 2.4s
=> [openwrt] docker-image://docker.io/docker/dockerfile:1.8@sha256:e87caa74dcb7d46cd820352bfea12591f3dba3ddc4285e19c7dcd13359f7cefd 3.2s
=> => resolve docker.io/docker/dockerfile:1.8@sha256:e87caa74dcb7d46cd820352bfea12591f3dba3ddc4285e19c7dcd13359f7cefd 0.0s
=> => sha256:1d7e21b9eae3317824ebc8707e591a810a37dff6aff8e1d175f7d8c95a3aa532 12.09MB / 12.09MB 2.4s
=> => sha256:e87caa74dcb7d46cd820352bfea12591f3dba3ddc4285e19c7dcd13359f7cefd 8.40kB / 8.40kB 0.0s
=> => sha256:dde28735fbb848299b7a43b7561d828b89c76f84f745c31896a7a7efbf93851a 482B / 482B 0.0s
=> => sha256:265d11c3a0f3387fc23a28141d04918dae4cfa0b0ff311f958e05b23f772db95 1.26kB / 1.26kB 0.0s
=> => extracting sha256:1d7e21b9eae3317824ebc8707e591a810a37dff6aff8e1d175f7d8c95a3aa532 0.7s=> ERROR [openwrt internal] load metadata for docker.io/openwrt/rootfs:23.05.4 1.1s
[openwrt internal] load metadata for docker.io/openwrt/rootfs:23.05.4:
failed to solve: openwrt/rootfs:23.05.4: docker.io/openwrt/rootfs:23.05.4: not found
Ошибка openwrt/rootfs:23.05.4: not found возникла по двум причинам:
- Формат тегов: В официальном репозитории Docker Hub (
openwrt/rootfs) теги включают архитектуру. Тега23.05.4не существует, есть теги видаx86-64-23.05.4илиx86-64-23.05.5. - Смена репозитория: Разработчики OpenWrt недавно перенесли образы с
openwrtorg/rootfsнаopenwrt/rootfs, что часто вызывает путаницу с кэшем Docker.
Чтобы избежать подобных проблем в будущем и не зависеть от тегов Docker Hub, самый надежный способ — это собирать образ самостоятельно, скачивая официальный архив rootfs напрямую с сервера OpenWrt и импортируя его в Docker.
Ниже приведены обновленные конфигурационные файлы. Этот подход гарантирует, что вы получите именно ту версию OpenWrt и архитектуру (x86_64), которая вам нужна.
1. Обновленный Dockerfile
Замените содержимое вашего Dockerfile на следующее. Здесь используется FROM scratch, скачивается официальный rootfs и устанавливаются все необходимые пакеты.
# syntax=docker/dockerfile:1.8
ARG OPENWRT_VERSION=23.05.5
ARG TARGET_ARCH=x86-64
# Ссылка на официальный архив rootfs от OpenWrt
ARG ROOTFS_URL=https://downloads.openwrt.org/releases/${OPENWRT_VERSION}/targets/x86/64/openwrt-${OPENWRT_VERSION}-${TARGET_ARCH}-rootfs.tar.gz
FROM scratch AS base
ARG ROOTFS_URL
# Скачиваем и распаковываем официальный rootfs напрямую с сервера OpenWrt
ADD ${ROOTFS_URL} /
FROM base AS openwrt
ARG ROOT_PASSWORD=ChangeMe_Strong
ENV TZ=UTC
# OpenWRT требует некоторых базовых директорий для работы в контейнере
RUN mkdir -p /var/lock /var/run /tmp /rom \
&& cp -a /etc /rom/ \
&& ln -sf /proc/mounts /etc/mtab
# Устанавливаем пакеты, необходимые для работы роутера и Web-интерфейса (LuCI)
RUN opkg update \
&& opkg install --force-overwrite --force-space \
luci-ssl \
uhttpd \
uhttpd-mod-ubus \
dnsmasq \
firewall4 \
nftables \
dropbear \
ip-full \
ca-bundle \
openssl-util \
&& rm -rf /var/opkg-lists /tmp/*
# Устанавливаем пароль root
RUN echo "root:${ROOT_PASSWORD}" | chpasswd
# Включаем автозапуск нужных сервисов
RUN for s in boot network firewall dnsmasq dropbear uhttpd rpcd sysctl; do \
if [ -x "/etc/init.d/$s" ]; then "/etc/init.d/$s" enable || true; fi; \
done
COPY <<'EOF' /entrypoint.sh
#!/bin/sh
set -e
: "${HOSTNAME:=openwrt}"
: "${WAN_MAC:=02:42:c0:a8:00:c8}"
: "${LAN_MAC:=02:42:c0:a8:c8:01}"
: "${WAN_IP:=192.168.0.200}"
: "${WAN_NETMASK:=255.255.255.0}"
: "${WAN_GATEWAY:=192.168.0.1}"
: "${WAN_DNS:=192.168.0.1}"
: "${LAN_IP:=192.168.200.1}"
: "${LAN_NETMASK:=255.255.255.0}"
: "${WAN_WEB_ALLOW:=192.168.0.0/24}"
if [ -n "${ROOT_PASSWORD:-}" ] && [ ! -f /etc/.docker_root_password_set ]; then
echo "root:${ROOT_PASSWORD}" | chpasswd
touch /etc/.docker_root_password_set
fi
hostname "${HOSTNAME}" >/dev/null 2>&1 || true
non_lo_ifs() {
ip -o link show 2>/dev/null | awk -F': ' '{print $2}' | cut -d'@' -f1 | grep -E '^eth[0-9]+$' || true
}
i=0
while [ "$i" -lt 15 ]; do
cnt=$(non_lo_ifs | wc -l)
[ "$cnt" -ge 2 ] && break
sleep 1
i=$((i+1))
done
find_if_by_mac() {
want=$(echo "$1" | tr 'A-F' 'a-f')
for p in /sys/class/net/*; do
n="${p##*/}"
[ "$n" = "lo" ] && continue
mac=$(cat "$p/address" 2>/dev/null || true)
mac=$(echo "$mac" | tr 'A-F' 'a-f')
if [ -n "$mac" ] && [ "$mac" = "$want" ]; then
echo "$n"
return 0
fi
done
return 1
}
WAN_IF=$(find_if_by_mac "$WAN_MAC" || true)
LAN_IF=$(find_if_by_mac "$LAN_MAC" || true)
if [ -z "$WAN_IF" ] || [ -z "$LAN_IF" ] || [ "$WAN_IF" = "$LAN_IF" ]; then
set -- $(non_lo_ifs)
if [ -z "$WAN_IF" ]; then
WAN_IF="$1"
fi
if [ -z "$LAN_IF" ] || [ "$LAN_IF" = "$WAN_IF" ]; then
shift 2>/dev/null || true
LAN_IF="$1"
fi
fi
: "${WAN_IF:=eth0}"
: "${LAN_IF:=eth1}"
if [ "$WAN_IF" = "$LAN_IF" ]; then
LAN_IF="eth1"
fi
echo "OpenWRT container: WAN=${WAN_IF}, LAN=${LAN_IF}"
cat > /etc/config/network <<NETWORK
config interface 'loopback'
option device 'lo'
option proto 'static'
option ipaddr '127.0.0.1'
option netmask '255.0.0.0'
config interface 'wan'
option device '${WAN_IF}'
option proto 'static'
option ipaddr '${WAN_IP}'
option netmask '${WAN_NETMASK}'
option gateway '${WAN_GATEWAY}'
list dns '${WAN_DNS}'
option defaultroute '1'
config interface 'lan'
option device '${LAN_IF}'
option proto 'static'
option ipaddr '${LAN_IP}'
option netmask '${LAN_NETMASK}'
NETWORK
while ip route del default 2>/dev/null; do :; done
if [ -f /etc/config/system ]; then
(
set +e
uci -q set system.@system[0].hostname="${HOSTNAME}"
uci commit system
)
fi
if [ -f /etc/config/dhcp ]; then
(
set +e
uci -q set dhcp.lan=dhcp
uci set dhcp.lan.interface=lan
uci set dhcp.lan.start=100
uci set dhcp.lan.limit=150
uci set dhcp.lan.leasetime=12h
uci -q set dhcp.wan=dhcp
uci set dhcp.wan.interface=wan
uci set dhcp.wan.ignore=1
uci commit dhcp
)
fi
ensure_web_rule() {
name="$1"
port="$2"
if ! uci -q get "firewall.${name}" >/dev/null 2>&1; then
uci set "firewall.${name}=rule"
fi
uci set "firewall.${name}.name=${name}"
uci set "firewall.${name}.src=wan"
uci set "firewall.${name}.proto=tcp"
uci set "firewall.${name}.dest_port=${port}"
uci set "firewall.${name}.target=ACCEPT"
if [ -n "${WAN_WEB_ALLOW}" ]; then
uci set "firewall.${name}.src_ip=${WAN_WEB_ALLOW}"
else
uci -q delete "firewall.${name}.src_ip"
fi
}
if [ -f /etc/config/firewall ]; then
(
set +e
ensure_web_rule allow_http_wan 80
ensure_web_rule allow_https_wan 443
uci commit firewall
)
fi
if [ ! -s /etc/uhttpd.crt ] || [ ! -s /etc/uhttpd.key ]; then
if command -v openssl >/dev/null 2>&1; then
openssl req -x509 -newkey rsa:2048 -nodes -days 825 \
-keyout /etc/uhttpd.key -out /etc/uhttpd.crt \
-subj "/CN=${HOSTNAME}" >/dev/null 2>&1 || true
chmod 600 /etc/uhttpd.key 2>/dev/null || true
fi
fi
exec /sbin/init
EOF
RUN chmod +x /entrypoint.sh
EXPOSE 80 443
ENTRYPOINT ["/entrypoint.sh"]
2. Обновленный docker-compose.yml
Внесите изменения в секцию build.args. Убран неиспользуемый OPENWRT_TAG и добавлен OPENWRT_VERSION.
services:
openwrt:
build:
context: .
args:
OPENWRT_VERSION: "23.05.5"
ROOT_PASSWORD: "ChangeMe_Strong"
image: openwrt-macvlan:23.05
container_name: openwrt
hostname: openwrt
restart: unless-stopped
stop_grace_period: 30s
privileged: true
environment:
TZ: "UTC"
HOSTNAME: "openwrt"
WAN_MAC: "02:42:c0:a8:00:c8"
LAN_MAC: "02:42:c0:a8:c8:01"
WAN_IP: "192.168.0.200"
WAN_NETMASK: "255.255.255.0"
WAN_GATEWAY: "192.168.0.1"
WAN_DNS: "192.168.0.1"
LAN_IP: "192.168.200.1"
LAN_NETMASK: "255.255.255.0"
WAN_WEB_ALLOW: "192.168.0.0/24"
volumes:
- /lib/modules:/lib/modules:ro
- openwrt-etc:/etc
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.rp_filter=0
- net.ipv4.conf.default.rp_filter=0
- net.ipv4.conf.all.send_redirects=0
- net.ipv4.conf.default.send_redirects=0
- net.ipv4.conf.all.accept_redirects=0
- net.ipv4.conf.default.accept_redirects=0
networks:
wan:
ipv4_address: 192.168.0.200
mac_address: "02:42:c0:a8:00:c8"
priority: 1000
lan:
ipv4_address: 192.168.200.1
mac_address: "02:42:c0:a8:c8:01"
priority: 900
networks:
wan:
driver: macvlan
driver_opts:
parent: enp6s0
enable_ipv6: false
ipam:
config:
- subnet: 192.168.0.0/24
gateway: 192.168.0.1
ip_range: 192.168.0.200/32
lan:
driver: macvlan
driver_opts:
parent: enp7s0
enable_ipv6: false
ipam:
config:
- subnet: 192.168.200.0/24
gateway: 192.168.200.254
ip_range: 192.168.200.1/32
volumes:
openwrt-etc:
3. Как пересобрать образ
Теперь выполните следующие команды для принудительной пересборки (без использования старого кэша, который мог запомнить ошибку):
# 1. Остановите и удалите старый контейнер и сеть
docker compose down
# 2. Полностью пересоберите образ, игнорируя старый кэш
docker compose build --no-cache --pull
# 3. Запустите контейнер
docker compose up -d
Что изменилось в новой версии:
- Надежность: Мы больше не зависим от того, успели ли мейнтейнеры OpenWrt залить нужный тег на Docker Hub. Мы качаем оригинальный архив с
downloads.openwrt.org. - Архитектура: Явно задана архитектура
x86-64(так как ваши интерфейсыenp...говорят о том, что это стандартный x86_64 сервер/ПК). - Чистота: Пакеты ставятся один раз при сборке образа, а не каждый раз при запуске контейнера, что ускоряет старт.
root@panel:/opt/openwrt# docker compose build —no-cache —pull
[+] Building 2.6s (8/13) docker:default
=> [openwrt internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 5.63kB 0.0s
=> [openwrt] resolve image config for docker-image://docker.io/docker/dockerfile:1.8 1.2s
=> CACHED [openwrt] docker-image://docker.io/docker/dockerfile:1.8@sha256:e87caa74dcb7d46cd820352bfea12591f3dba3ddc4285e19c7dcd13359f7cefd 0.0s
=> [openwrt internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [openwrt base 1/1] ADD https://downloads.openwrt.org/releases/23.05.5/targets/x86/64/openwrt-23.05.5-x86-64-rootfs.tar.gz / 1.0s
=> [openwrt internal] preparing inline document 0.0s
=> [openwrt base 1/1] ADD https://downloads.openwrt.org/releases/23.05.5/targets/x86/64/openwrt-23.05.5-x86-64-rootfs.tar.gz / 0.0s=> ERROR [openwrt openwrt 1/6] RUN mkdir -p /var/lock /var/run /tmp /rom && cp -a /etc /rom/ && ln -sf /proc/mounts /etc/mtab 0.2s
[openwrt openwrt 1/6] RUN mkdir -p /var/lock /var/run /tmp /rom && cp -a /etc /rom/ && ln -sf /proc/mounts /etc/mtab:
0.202 runc run failed: unable to start container process: exec: «/bin/sh»: stat /bin/sh: no such file or directory
failed to solve: process «/bin/sh -c mkdir -p /var/lock /var/run /tmp /rom && cp -a /etc /rom/ && ln -sf /proc/mounts /etc/mtab» did not complete successfully: exit code: 1
root@panel:/opt/openwrt#
Ошибка exec: "/bin/sh": stat /bin/sh: no such file or directory возникает по одной конкретной причине: в официальном архиве rootfs OpenWrt отсутствует файл /bin/sh.
По умолчанию Docker при сборке пытается выполнять все инструкции RUN через /bin/sh. В стандартных дистрибутивах (Alpine, Ubuntu) /bin/sh существует, но в «чистом» rootfs OpenWrt его нет — есть только /bin/ash (или симлинки на busybox). Из-за этого сборщик Docker просто не может выполнить ни одной команды после распаковки архива.
Решение
Нужно явно указать Docker использовать /bin/ash вместо /bin/sh для всех команд сборки. Это делается одной инструкцией SHELL.
Вот полностью исправленный Dockerfile:
# syntax=docker/dockerfile:1.8
ARG OPENWRT_VERSION=23.05.5
ARG TARGET_ARCH=x86-64
ARG ROOTFS_URL=https://downloads.openwrt.org/releases/${OPENWRT_VERSION}/targets/x86/64/openwrt-${OPENWRT_VERSION}-${TARGET_ARCH}-rootfs.tar.gz
FROM scratch AS base
ARG ROOTFS_URL
# Скачиваем и распаковываем официальный rootfs напрямую с сервера OpenWrt
ADD ${ROOTFS_URL} /
FROM base AS openwrt
ARG ROOT_PASSWORD=ChangeMe_Strong
ENV TZ=UTC
# КРИТИЧЕСКИ ВАЖНО: В OpenWrt rootfs нет /bin/sh, поэтому переключаем shell на /bin/ash
SHELL ["/bin/ash", "-c"]
# OpenWRT требует некоторых базовых директорий для работы в контейнере
RUN mkdir -p /var/lock /var/run /tmp /rom \
&& cp -a /etc /rom/ \
&& ln -sf /proc/mounts /etc/mtab
# Устанавливаем пакеты, необходимые для работы роутера и Web-интерфейса (LuCI)
RUN opkg update \
&& opkg install --force-overwrite --force-space \
luci-ssl \
uhttpd \
uhttpd-mod-ubus \
dnsmasq \
firewall4 \
nftables \
dropbear \
ip-full \
ca-bundle \
openssl-util \
&& rm -rf /var/opkg-lists /tmp/*
# Устанавливаем пароль root
RUN echo "root:${ROOT_PASSWORD}" | chpasswd
# Включаем автозапуск нужных сервисов
RUN for s in boot network firewall dnsmasq dropbear uhttpd rpcd sysctl; do \
if [ -x "/etc/init.d/$s" ]; then "/etc/init.d/$s" enable || true; fi; \
done
COPY <<'EOF' /entrypoint.sh
#!/bin/sh
set -e
: "${HOSTNAME:=openwrt}"
: "${WAN_MAC:=02:42:c0:a8:00:c8}"
: "${LAN_MAC:=02:42:c0:a8:c8:01}"
: "${WAN_IP:=192.168.0.200}"
: "${WAN_NETMASK:=255.255.255.0}"
: "${WAN_GATEWAY:=192.168.0.1}"
: "${WAN_DNS:=192.168.0.1}"
: "${LAN_IP:=192.168.200.1}"
: "${LAN_NETMASK:=255.255.255.0}"
: "${WAN_WEB_ALLOW:=192.168.0.0/24}"
if [ -n "${ROOT_PASSWORD:-}" ] && [ ! -f /etc/.docker_root_password_set ]; then
echo "root:${ROOT_PASSWORD}" | chpasswd
touch /etc/.docker_root_password_set
fi
hostname "${HOSTNAME}" >/dev/null 2>&1 || true
non_lo_ifs() {
ip -o link show 2>/dev/null | awk -F': ' '{print $2}' | cut -d'@' -f1 | grep -E '^eth[0-9]+$' || true
}
i=0
while [ "$i" -lt 15 ]; do
cnt=$(non_lo_ifs | wc -l)
[ "$cnt" -ge 2 ] && break
sleep 1
i=$((i+1))
done
find_if_by_mac() {
want=$(echo "$1" | tr 'A-F' 'a-f')
for p in /sys/class/net/*; do
n="${p##*/}"
[ "$n" = "lo" ] && continue
mac=$(cat "$p/address" 2>/dev/null || true)
mac=$(echo "$mac" | tr 'A-F' 'a-f')
if [ -n "$mac" ] && [ "$mac" = "$want" ]; then
echo "$n"
return 0
fi
done
return 1
}
WAN_IF=$(find_if_by_mac "$WAN_MAC" || true)
LAN_IF=$(find_if_by_mac "$LAN_MAC" || true)
if [ -z "$WAN_IF" ] || [ -z "$LAN_IF" ] || [ "$WAN_IF" = "$LAN_IF" ]; then
set -- $(non_lo_ifs)
if [ -z "$WAN_IF" ]; then
WAN_IF="$1"
fi
if [ -z "$LAN_IF" ] || [ "$LAN_IF" = "$WAN_IF" ]; then
shift 2>/dev/null || true
LAN_IF="$1"
fi
fi
: "${WAN_IF:=eth0}"
: "${LAN_IF:=eth1}"
if [ "$WAN_IF" = "$LAN_IF" ]; then
LAN_IF="eth1"
fi
echo "OpenWRT container: WAN=${WAN_IF}, LAN=${LAN_IF}"
cat > /etc/config/network <<NETWORK
config interface 'loopback'
option device 'lo'
option proto 'static'
option ipaddr '127.0.0.1'
option netmask '255.0.0.0'
config interface 'wan'
option device '${WAN_IF}'
option proto 'static'
option ipaddr '${WAN_IP}'
option netmask '${WAN_NETMASK}'
option gateway '${WAN_GATEWAY}'
list dns '${WAN_DNS}'
option defaultroute '1'
config interface 'lan'
option device '${LAN_IF}'
option proto 'static'
option ipaddr '${LAN_IP}'
option netmask '${LAN_NETMASK}'
NETWORK
while ip route del default 2>/dev/null; do :; done
if [ -f /etc/config/system ]; then
(
set +e
uci -q set system.@system[0].hostname="${HOSTNAME}"
uci commit system
)
fi
if [ -f /etc/config/dhcp ]; then
(
set +e
uci -q set dhcp.lan=dhcp
uci set dhcp.lan.interface=lan
uci set dhcp.lan.start=100
uci set dhcp.lan.limit=150
uci set dhcp.lan.leasetime=12h
uci -q set dhcp.wan=dhcp
uci set dhcp.wan.interface=wan
uci set dhcp.wan.ignore=1
uci commit dhcp
)
fi
ensure_web_rule() {
name="$1"
port="$2"
if ! uci -q get "firewall.${name}" >/dev/null 2>&1; then
uci set "firewall.${name}=rule"
fi
uci set "firewall.${name}.name=${name}"
uci set "firewall.${name}.src=wan"
uci set "firewall.${name}.proto=tcp"
uci set "firewall.${name}.dest_port=${port}"
uci set "firewall.${name}.target=ACCEPT"
if [ -n "${WAN_WEB_ALLOW}" ]; then
uci set "firewall.${name}.src_ip=${WAN_WEB_ALLOW}"
else
uci -q delete "firewall.${name}.src_ip"
fi
}
if [ -f /etc/config/firewall ]; then
(
set +e
ensure_web_rule allow_http_wan 80
ensure_web_rule allow_https_wan 443
uci commit firewall
)
fi
if [ ! -s /etc/uhttpd.crt ] || [ ! -s /etc/uhttpd.key ]; then
if command -v openssl >/dev/null 2>&1; then
openssl req -x509 -newkey rsa:2048 -nodes -days 825 \
-keyout /etc/uhttpd.key -out /etc/uhttpd.crt \
-subj "/CN=${HOSTNAME}" >/dev/null 2>&1 || true
chmod 600 /etc/uhttpd.key 2>/dev/null || true
fi
fi
exec /sbin/init
EOF
RUN chmod +x /entrypoint.sh
EXPOSE 80 443
ENTRYPOINT ["/entrypoint.sh"]
Что изменилось:
- Добавлена строка
SHELL ["/bin/ash", "-c"]. Теперь все командыRUNбудут выполняться через/bin/ash, который точно есть в архиве. - Убран лишний
COPY(все делается черезADD). - Файл
docker-compose.ymlостается без изменений.
Запуск сборки:
Теперь выполните сборку заново:
docker compose build --no-cache
docker compose up -d
Сборка может занять 1-3 минуты, так как opkg update && opkg install будет скачивать и устанавливать все пакеты LuCI и firewall прямо в образ. После успешной сборки контейнер сразу поднимется с готовой конфигурацией.
root@panel:/opt/openwrt# docker compose build —no-cache
[+] Building 1.7s (8/13) docker:default
=> [openwrt internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 5.71kB 0.0s
=> [openwrt] resolve image config for docker-image://docker.io/docker/dockerfile:1.8 1.1s
=> CACHED [openwrt] docker-image://docker.io/docker/dockerfile:1.8@sha256:e87caa74dcb7d46cd820352bfea12591f3dba3ddc4285e19c7dcd13359f7cefd 0.0s
=> [openwrt internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> CACHED [openwrt internal] preparing inline document 0.0s
=> CACHED [openwrt base 1/1] ADD https://downloads.openwrt.org/releases/23.05.5/targets/x86/64/openwrt-23.05.5-x86-64-rootfs.tar.gz / 0.3s
=> [openwrt base 1/1] ADD https://downloads.openwrt.org/releases/23.05.5/targets/x86/64/openwrt-23.05.5-x86-64-rootfs.tar.gz / 0.0s=> ERROR [openwrt openwrt 1/6] RUN mkdir -p /var/lock /var/run /tmp /rom && cp -a /etc /rom/ && ln -sf /proc/mounts /etc/mtab 0.2s
[openwrt openwrt 1/6] RUN mkdir -p /var/lock /var/run /tmp /rom && cp -a /etc /rom/ && ln -sf /proc/mounts /etc/mtab:
0.197 runc run failed: unable to start container process: exec: «/bin/ash»: stat /bin/ash: no such file or directory
failed to solve: process «/bin/ash -c mkdir -p /var/lock /var/run /tmp /rom && cp -a /etc /rom/ && ln -sf /proc/mounts /etc/mtab» did not complete successfully: exit code: 1
root@panel:/opt/openwrt#
Ошибка stat /bin/ash: no such file or directory возникает из-за особенности инструкции ADD в Docker.
Когда вы используете ADD с URL-адресом (например, ADD https://.../rootfs.tar.gz /), Docker скачивает файл, но не распаковывает его. В результате внутри образа оказывался один архивный файл, а не файловая система OpenWrt. Поэтому ни /bin/sh, ни /bin/ash не существовали, и Docker не мог выполнить ни одну команду RUN.
Решение: Многоэтапная сборка (Multi-stage Build)
Чтобы гарантированно распаковать архив, мы будем использовать двухэтапную сборку:
- Stage 1 (Alpine): Скачивает архив по URL и распаковывает его в чистую директорию. Alpine точно имеет
curlиtar. - Stage 2 (Scratch): Копирует уже распакованную файловую систему из Alpine в финальный пустой образ
scratch.
Исправленный Dockerfile
Замените содержимое вашего Dockerfile на этот код. Он полностью самодостаточен и не зависит от наличия тегов на Docker Hub.
# syntax=docker/dockerfile:1.8
ARG OPENWRT_VERSION=23.05.5
ARG TARGET_ARCH=x86-64
# Ссылка на официальный архив rootfs
ARG ROOTFS_URL=https://downloads.openwrt.org/releases/${OPENWRT_VERSION}/targets/x86/64/openwrt-${OPENWRT_VERSION}-${TARGET_ARCH}-rootfs.tar.gz
# --- ЭТАП 1: Скачивание и распаковка ---
FROM alpine:3.19 AS downloader
ARG ROOTFS_URL
# Устанавливаем curl и tar, скачиваем архив и распаковываем его в /rootfs
RUN apk add --no-cache curl tar \
&& mkdir -p /rootfs \
&& curl -sSL ${ROOTFS_URL} | tar -xz -C /rootfs
# --- ЭТАП 2: Сборка финального образа ---
FROM scratch AS base
# Копируем распакованную файловую систему из этапа 1
COPY --from=downloader /rootfs /
FROM base AS openwrt
# В OpenWrt нет /bin/sh, поэтому переключаем shell для всех RUN команд на /bin/ash
SHELL ["/bin/ash", "-c"]
ARG ROOT_PASSWORD=ChangeMe_Strong
ENV TZ=UTC
# Создаем необходимые директории и симлинки для работы OpenWrt в контейнере
RUN mkdir -p /var/lock /var/run /tmp /rom \
&& cp -a /etc /rom/ \
&& ln -sf /proc/mounts /etc/mtab
# Устанавливаем пакеты для Web-интерфейса (LuCI) и маршрутизации
RUN opkg update \
&& opkg install --force-overwrite --force-space \
luci-ssl \
uhttpd \
uhttpd-mod-ubus \
dnsmasq \
firewall4 \
nftables \
dropbear \
ip-full \
ca-bundle \
openssl-util \
&& rm -rf /var/opkg-lists /tmp/*
# Устанавливаем пароль root
RUN echo "root:${ROOT_PASSWORD}" | chpasswd
# Включаем автозапуск сервисов
RUN for s in boot network firewall dnsmasq dropbear uhttpd rpcd sysctl; do \
if [ -x "/etc/init.d/$s" ]; then "/etc/init.d/$s" enable || true; fi; \
done
# Entrypoint-скрипт для настройки сети и фаервола при каждом запуске
COPY <<'EOF' /entrypoint.sh
#!/bin/sh
set -e
: "${HOSTNAME:=openwrt}"
: "${WAN_MAC:=02:42:c0:a8:00:c8}"
: "${LAN_MAC:=02:42:c0:a8:c8:01}"
: "${WAN_IP:=192.168.0.200}"
: "${WAN_NETMASK:=255.255.255.0}"
: "${WAN_GATEWAY:=192.168.0.1}"
: "${WAN_DNS:=192.168.0.1}"
: "${LAN_IP:=192.168.200.1}"
: "${LAN_NETMASK:=255.255.255.0}"
: "${WAN_WEB_ALLOW:=192.168.0.0/24}"
if [ -n "${ROOT_PASSWORD:-}" ] && [ ! -f /etc/.docker_root_password_set ]; then
echo "root:${ROOT_PASSWORD}" | chpasswd
touch /etc/.docker_root_password_set
fi
hostname "${HOSTNAME}" >/dev/null 2>&1 || true
non_lo_ifs() {
ip -o link show 2>/dev/null | awk -F': ' '{print $2}' | cut -d'@' -f1 | grep -E '^eth[0-9]+$' || true
}
i=0
while [ "$i" -lt 15 ]; do
cnt=$(non_lo_ifs | wc -l)
[ "$cnt" -ge 2 ] && break
sleep 1
i=$((i+1))
done
find_if_by_mac() {
want=$(echo "$1" | tr 'A-F' 'a-f')
for p in /sys/class/net/*; do
n="${p##*/}"
[ "$n" = "lo" ] && continue
mac=$(cat "$p/address" 2>/dev/null || true)
mac=$(echo "$mac" | tr 'A-F' 'a-f')
if [ -n "$mac" ] && [ "$mac" = "$want" ]; then
echo "$n"
return 0
fi
done
return 1
}
WAN_IF=$(find_if_by_mac "$WAN_MAC" || true)
LAN_IF=$(find_if_by_mac "$LAN_MAC" || true)
if [ -z "$WAN_IF" ] || [ -z "$LAN_IF" ] || [ "$WAN_IF" = "$LAN_IF" ]; then
set -- $(non_lo_ifs)
if [ -z "$WAN_IF" ]; then
WAN_IF="$1"
fi
if [ -z "$LAN_IF" ] || [ "$LAN_IF" = "$WAN_IF" ]; then
shift 2>/dev/null || true
LAN_IF="$1"
fi
fi
: "${WAN_IF:=eth0}"
: "${LAN_IF:=eth1}"
if [ "$WAN_IF" = "$LAN_IF" ]; then
LAN_IF="eth1"
fi
echo "OpenWRT container: WAN=${WAN_IF}, LAN=${LAN_IF}"
cat > /etc/config/network <<NETWORK
config interface 'loopback'
option device 'lo'
option proto 'static'
option ipaddr '127.0.0.1'
option netmask '255.0.0.0'
config interface 'wan'
option device '${WAN_IF}'
option proto 'static'
option ipaddr '${WAN_IP}'
option netmask '${WAN_NETMASK}'
option gateway '${WAN_GATEWAY}'
list dns '${WAN_DNS}'
option defaultroute '1'
config interface 'lan'
option device '${LAN_IF}'
option proto 'static'
option ipaddr '${LAN_IP}'
option netmask '${LAN_NETMASK}'
NETWORK
while ip route del default 2>/dev/null; do :; done
if [ -f /etc/config/system ]; then
(
set +e
uci -q set system.@system[0].hostname="${HOSTNAME}"
uci commit system
)
fi
if [ -f /etc/config/dhcp ]; then
(
set +e
uci -q set dhcp.lan=dhcp
uci set dhcp.lan.interface=lan
uci set dhcp.lan.start=100
uci set dhcp.lan.limit=150
uci set dhcp.lan.leasetime=12h
uci -q set dhcp.wan=dhcp
uci set dhcp.wan.interface=wan
uci set dhcp.wan.ignore=1
uci commit dhcp
)
fi
ensure_web_rule() {
name="$1"
port="$2"
if ! uci -q get "firewall.${name}" >/dev/null 2>&1; then
uci set "firewall.${name}=rule"
fi
uci set "firewall.${name}.name=${name}"
uci set "firewall.${name}.src=wan"
uci set "firewall.${name}.proto=tcp"
uci set "firewall.${name}.dest_port=${port}"
uci set "firewall.${name}.target=ACCEPT"
if [ -n "${WAN_WEB_ALLOW}" ]; then
uci set "firewall.${name}.src_ip=${WAN_WEB_ALLOW}"
else
uci -q delete "firewall.${name}.src_ip"
fi
}
if [ -f /etc/config/firewall ]; then
(
set +e
ensure_web_rule allow_http_wan 80
ensure_web_rule allow_https_wan 443
uci commit firewall
)
fi
if [ ! -s /etc/uhttpd.crt ] || [ ! -s /etc/uhttpd.key ]; then
if command -v openssl >/dev/null 2>&1; then
openssl req -x509 -newkey rsa:2048 -nodes -days 825 \
-keyout /etc/uhttpd.key -out /etc/uhttpd.crt \
-subj "/CN=${HOSTNAME}" >/dev/null 2>&1 || true
chmod 600 /etc/uhttpd.key 2>/dev/null || true
fi
fi
exec /sbin/init
EOF
RUN chmod +x /entrypoint.sh
EXPOSE 80 443
ENTRYPOINT ["/entrypoint.sh"]
Файл docker-compose.yml (без изменений)
Оставьте его таким же, как в предыдущем ответе. Он не требует изменений.
Как запустить сборку
Перед сборкой обязательно нужно очистить старые слои кэша, чтобы Docker не пытался использовать битые промежуточные слои:
# 1. Остановите и удалите старые контейнеры и сети
docker compose down
# 2. Удалите старые неиспользуемые образы и кэш сборки
docker builder prune -af
# 3. Соберите образ заново с нуля
docker compose build --no-cache
# 4. Запустите контейнер
docker compose up -d
Теперь сборка должна пройти успешно. Этап downloader скачает архив, распакует его, а затем opkg установит все необходимые пакеты (LuCI, firewall4, dropbear) прямо в файловую систему. После запуска контейнер сразу будет готов к работе с настроенными сетевыми интерфейсами и открытым Web-интерфейсом.
root@panel:/opt/openwrt# docker compose build —no-cache
[+] Building 5.2s (3/3) FINISHED docker:default
=> [openwrt internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 4.27kB 0.0s
=> [openwrt] resolve image config for docker-image://docker.io/docker/dockerfile:1.8 2.0s
=> [openwrt] docker-image://docker.io/docker/dockerfile:1.8@sha256:e87caa74dcb7d46cd820352bfea12591f3dba3ddc4285e19c7dcd13359f7cefd 3.1s
=> => resolve docker.io/docker/dockerfile:1.8@sha256:e87caa74dcb7d46cd820352bfea12591f3dba3ddc4285e19c7dcd13359f7cefd 0.0s
=> => sha256:1d7e21b9eae3317824ebc8707e591a810a37dff6aff8e1d175f7d8c95a3aa532 12.09MB / 12.09MB 2.3s
=> => sha256:e87caa74dcb7d46cd820352bfea12591f3dba3ddc4285e19c7dcd13359f7cefd 8.40kB / 8.40kB 0.0s
=> => sha256:dde28735fbb848299b7a43b7561d828b89c76f84f745c31896a7a7efbf93851a 482B / 482B 0.0s
=> => sha256:265d11c3a0f3387fc23a28141d04918dae4cfa0b0ff311f958e05b23f772db95 1.26kB / 1.26kB 0.0s
=> => extracting sha256:1d7e21b9eae3317824ebc8707e591a810a37dff6aff8e1d175f7d8c95a3aa532 0.7s
failed to solve: unterminated heredoc
root@panel:/opt/openwrt#
Ошибка failed to solve: unterminated heredoc возникает из-за того, что при копировании текста из чата в Dockerfile редактор часто добавляет лишние пробелы или символы табуляции перед закрывающим маркером EOF (или NETWORK). Парсер Dockerfile требует, чтобы закрывающий маркер находился строго в самом начале строки без каких-либо отступов.
Самый надежный и чистый способ избавиться от этой ошибки навсегда — вынести скрипт инициализации в отдельный файл в вашей директории и просто скопировать его в образ.
В вашей директории /opt/openwrt должно теперь находиться три файла:
Dockerfiledocker-compose.ymlentrypoint.sh(новый файл)
1. Создайте файл entrypoint.sh
Создайте в текущей директории файл с именем entrypoint.sh и вставьте в него следующий код:
#!/bin/sh
set -e
: "${HOSTNAME:=openwrt}"
: "${WAN_MAC:=02:42:c0:a8:00:c8}"
: "${LAN_MAC:=02:42:c0:a8:c8:01}"
: "${WAN_IP:=192.168.0.200}"
: "${WAN_NETMASK:=255.255.255.0}"
: "${WAN_GATEWAY:=192.168.0.1}"
: "${WAN_DNS:=192.168.0.1}"
: "${LAN_IP:=192.168.200.1}"
: "${LAN_NETMASK:=255.255.255.0}"
: "${WAN_WEB_ALLOW:=192.168.0.0/24}"
if [ -n "${ROOT_PASSWORD:-}" ] && [ ! -f /etc/.docker_root_password_set ]; then
echo "root:${ROOT_PASSWORD}" | chpasswd
touch /etc/.docker_root_password_set
fi
hostname "${HOSTNAME}" >/dev/null 2>&1 || true
non_lo_ifs() {
ip -o link show 2>/dev/null | awk -F': ' '{print $2}' | cut -d'@' -f1 | grep -E '^eth[0-9]+$' || true
}
i=0
while [ "$i" -lt 15 ]; do
cnt=$(non_lo_ifs | wc -l)
[ "$cnt" -ge 2 ] && break
sleep 1
i=$((i+1))
done
find_if_by_mac() {
want=$(echo "$1" | tr 'A-F' 'a-f')
for p in /sys/class/net/*; do
n="${p##*/}"
[ "$n" = "lo" ] && continue
mac=$(cat "$p/address" 2>/dev/null || true)
mac=$(echo "$mac" | tr 'A-F' 'a-f')
if [ -n "$mac" ] && [ "$mac" = "$want" ]; then
echo "$n"
return 0
fi
done
return 1
}
WAN_IF=$(find_if_by_mac "$WAN_MAC" || true)
LAN_IF=$(find_if_by_mac "$LAN_MAC" || true)
if [ -z "$WAN_IF" ] || [ -z "$LAN_IF" ] || [ "$WAN_IF" = "$LAN_IF" ]; then
set -- $(non_lo_ifs)
if [ -z "$WAN_IF" ]; then
WAN_IF="$1"
fi
if [ -z "$LAN_IF" ] || [ "$LAN_IF" = "$WAN_IF" ]; then
shift 2>/dev/null || true
LAN_IF="$1"
fi
fi
: "${WAN_IF:=eth0}"
: "${LAN_IF:=eth1}"
if [ "$WAN_IF" = "$LAN_IF" ]; then
LAN_IF="eth1"
fi
echo "OpenWRT container: WAN=${WAN_IF}, LAN=${LAN_IF}"
# Обратите внимание: heredoc внутри самого скрипта bash/sh работает отлично,
# проблема была именно в heredoc внутри Dockerfile
cat > /etc/config/network <<NETWORK
config interface 'loopback'
option device 'lo'
option proto 'static'
option ipaddr '127.0.0.1'
option netmask '255.0.0.0'
config interface 'wan'
option device '${WAN_IF}'
option proto 'static'
option ipaddr '${WAN_IP}'
option netmask '${WAN_NETMASK}'
option gateway '${WAN_GATEWAY}'
list dns '${WAN_DNS}'
option defaultroute '1'
config interface 'lan'
option device '${LAN_IF}'
option proto 'static'
option ipaddr '${LAN_IP}'
option netmask '${LAN_NETMASK}'
NETWORK
while ip route del default 2>/dev/null; do :; done
if [ -f /etc/config/system ]; then
(
set +e
uci -q set system.@system[0].hostname="${HOSTNAME}"
uci commit system
)
fi
if [ -f /etc/config/dhcp ]; then
(
set +e
uci -q set dhcp.lan=dhcp
uci set dhcp.lan.interface=lan
uci set dhcp.lan.start=100
uci set dhcp.lan.limit=150
uci set dhcp.lan.leasetime=12h
uci -q set dhcp.wan=dhcp
uci set dhcp.wan.interface=wan
uci set dhcp.wan.ignore=1
uci commit dhcp
)
fi
ensure_web_rule() {
name="$1"
port="$2"
if ! uci -q get "firewall.${name}" >/dev/null 2>&1; then
uci set "firewall.${name}=rule"
fi
uci set "firewall.${name}.name=${name}"
uci set "firewall.${name}.src=wan"
uci set "firewall.${name}.proto=tcp"
uci set "firewall.${name}.dest_port=${port}"
uci set "firewall.${name}.target=ACCEPT"
if [ -n "${WAN_WEB_ALLOW}" ]; then
uci set "firewall.${name}.src_ip=${WAN_WEB_ALLOW}"
else
uci -q delete "firewall.${name}.src_ip"
fi
}
if [ -f /etc/config/firewall ]; then
(
set +e
ensure_web_rule allow_http_wan 80
ensure_web_rule allow_https_wan 443
uci commit firewall
)
fi
if [ ! -s /etc/uhttpd.crt ] || [ ! -s /etc/uhttpd.key ]; then
if command -v openssl >/dev/null 2>&1; then
openssl req -x509 -newkey rsa:2048 -nodes -days 825 \
-keyout /etc/uhttpd.key -out /etc/uhttpd.crt \
-subj "/CN=${HOSTNAME}" >/dev/null 2>&1 || true
chmod 600 /etc/uhttpd.key 2>/dev/null || true
fi
fi
exec /sbin/init
2. Обновите Dockerfile
Замените ваш текущий Dockerfile на этот упрощенный вариант. Мы убрали из него весь многострочный текст и заменили его на простую команду COPY:
# syntax=docker/dockerfile:1.8
ARG OPENWRT_VERSION=23.05.5
ARG TARGET_ARCH=x86-64
ARG ROOTFS_URL=https://downloads.openwrt.org/releases/${OPENWRT_VERSION}/targets/x86/64/openwrt-${OPENWRT_VERSION}-${TARGET_ARCH}-rootfs.tar.gz
# --- ЭТАП 1: Скачивание и распаковка ---
FROM alpine:3.19 AS downloader
ARG ROOTFS_URL
RUN apk add --no-cache curl tar \
&& mkdir -p /rootfs \
&& curl -sSL ${ROOTFS_URL} | tar -xz -C /rootfs
# --- ЭТАП 2: Сборка финального образа ---
FROM scratch AS base
COPY --from=downloader /rootfs /
FROM base AS openwrt
# В OpenWrt нет /bin/sh, переключаемся на /bin/ash
SHELL ["/bin/ash", "-c"]
ARG ROOT_PASSWORD=ChangeMe_Strong
ENV TZ=UTC
RUN mkdir -p /var/lock /var/run /tmp /rom \
&& cp -a /etc /rom/ \
&& ln -sf /proc/mounts /etc/mtab
RUN opkg update \
&& opkg install --force-overwrite --force-space \
luci-ssl \
uhttpd \
uhttpd-mod-ubus \
dnsmasq \
firewall4 \
nftables \
dropbear \
ip-full \
ca-bundle \
openssl-util \
&& rm -rf /var/opkg-lists /tmp/*
RUN echo "root:${ROOT_PASSWORD}" | chpasswd
RUN for s in boot network firewall dnsmasq dropbear uhttpd rpcd sysctl; do \
if [ -x "/etc/init.d/$s" ]; then "/etc/init.d/$s" enable || true; fi; \
done
# Просто копируем готовый скрипт из папки в образ
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 80 443
ENTRYPOINT ["/entrypoint.sh"]
3. Запуск сборки
Теперь структура файлов в /opt/openwrt выглядит так:
Dockerfiledocker-compose.ymlentrypoint.sh
Запустите сборку и старт:
# Очищаем кэш и старые слои
docker builder prune -af
# Собираем
docker compose build --no-cache
# Запускаем
docker compose up -d
Теперь ошибка unterminated heredoc физически невозможна, так как Docker просто берет готовый файл entrypoint.sh и копирует его внутрь.
3.810 Downloading https://downloads.openwrt.org/releases/23.05.5/packages/x86_64/luci/Packages.sig
3.810 Signature check passed.
3.810 Downloading https://downloads.openwrt.org/releases/23.05.5/packages/x86_64/packages/Packages.gz
3.810 Updated list of available packages in /var/opkg-lists/openwrt_packages
3.810 Downloading https://downloads.openwrt.org/releases/23.05.5/packages/x86_64/packages/Packages.sig
3.810 Signature check passed.
5.428 Downloading https://downloads.openwrt.org/releases/23.05.5/packages/x86_64/routing/Packages.gz
5.428 Updated list of available packages in /var/opkg-lists/openwrt_routing
5.428 Downloading https://downloads.openwrt.org/releases/23.05.5/packages/x86_64/routing/Packages.sig
5.428 Signature check passed.
5.428 Downloading https://downloads.openwrt.org/releases/23.05.5/packages/x86_64/telephony/Packages.gz
5.428 Updated list of available packages in /var/opkg-lists/openwrt_telephony
5.428 Downloading https://downloads.openwrt.org/releases/23.05.5/packages/x86_64/telephony/Packages.sig
5.428 Signature check passed.
5.736 Upgrading luci-ssl on root from git-23.035.26083-7550ad6 to git-25.222.75657-7ce34fe…
7.654 Downloading https://downloads.openwrt.org/releases/23.05.5/packages/x86_64/luci/luci-ssl_git-25.222.75657-7ce34fe_all.ipk
7.654 Package uhttpd (2023-06-25-34a8a74d-2) installed in root is up to date.
7.654 Package uhttpd-mod-ubus (2023-06-25-34a8a74d-2) installed in root is up to date.
7.654 Package dnsmasq (2.90-2) installed in root is up to date.
7.654 Package firewall4 (2023-09-01-598d9fbb-1) installed in root is up to date.
7.654 Package nftables-json (1.0.8-1) installed in root is up to date.
7.654 Package dropbear (2022.82-6) installed in root is up to date.
7.654 Installing ip-full (6.3.0-1) to root…
7.654 Downloading https://downloads.openwrt.org/releases/23.05.5/packages/x86_64/base/ip-full_6.3.0-1_x86_64.ipk
7.654 Installing zlib (1.2.13-1) to root…
7.654 Downloading https://downloads.openwrt.org/releases/23.05.5/packages/x86_64/base/zlib_1.2.13-1_x86_64.ipk
7.654 Installing libelf1 (0.189-1) to root…
7.654 Downloading https://downloads.openwrt.org/releases/23.05.5/packages/x86_64/base/libelf1_0.189-1_x86_64.ipk
7.654 Installing libbpf1 (1.2.2-1) to root…
7.654 Downloading https://downloads.openwrt.org/releases/23.05.5/packages/x86_64/base/libbpf1_1.2.2-1_x86_64.ipkGenerating engines.cnf
11.24 Generating providers.cnf
11.28
11.28 Upgrading ca-bundle on root from 20230311-1 to 20241223-1…
11.28 Downloading https://downloads.openwrt.org/releases/23.05.5/packages/x86_64/base/ca-bundle_20241223-1_all.ipk
11.28 Installing openssl-util (3.0.16-1) to root…
11.28 Downloading https://downloads.openwrt.org/releases/23.05.5/packages/x86_64/base/openssl-util_3.0.16-1_x86_64.ipk
11.28 Installing libopenssl3 (3.0.16-1) to root…
11.28 Downloading https://downloads.openwrt.org/releases/23.05.5/packages/x86_64/base/libopenssl3_3.0.16-1_x86_64.ipk
11.28 Installing libopenssl-conf (3.0.16-1) to root…
11.28 Downloading https://downloads.openwrt.org/releases/23.05.5/packages/x86_64/base/libopenssl-conf_3.0.16-1_x86_64.ipk
11.28 Configuring libopenssl3.
11.28 Configuring zlib.
11.28 Configuring libelf1.
11.28 Configuring libbpf1.
11.28 Configuring ip-full.
11.28 Configuring libopenssl-conf.
11.28 Configuring openssl-util.
11.28 Configuring luci-ssl.
11.28 Configuring ca-bundle.11.28 rm: can’t remove ‘/tmp/resolv.conf’: Resource busy
failed to solve: process «/bin/ash -c opkg update && opkg install —force-overwrite —force-space luci-ssl uhttpd uhttpd-mod-ubus dnsmasq firewall4 nftables dropbear ip-full ca-bundle openssl-util && rm -rf /var/opkg-lists /tmp/*» did not complete successfully: exit code: 1
root@panel:/opt/openwrt#
Ошибка rm: can't remove '/tmp/resolv.conf': Resource busy — это классическая проблема при сборке OpenWrt в Docker.
Почему это происходит?
В файловой системе OpenWrt файл /etc/resolv.conf является симлинком (ссылкой) на /tmp/resolv.conf.
Когда Docker выполняет команды внутри контейнера (например, opkg update), он автоматически пробрасывает DNS-настройки хоста, чтобы контейнер мог резолвить доменные имена. Docker видит симлинк /etc/resolv.conf и монтирует файл с DNS-серверами хоста прямо поверх цели симлинка — то есть поверх /tmp/resolv.conf.
В результате /tmp/resolv.conf становится точкой монтирования (mount point). Когда скрипт пытается удалить всё содержимое /tmp/ командой rm -rf /tmp/*, он натыкается на примонтированный файл, получает отказ системы («Resource busy») и прерывает сборку.
Решение
Нужно просто игнорировать ошибку удаления для /tmp/*, так как это временная файловая система, и примонтированный файл всё равно исчезнет после завершения сборки.
Откройте ваш Dockerfile и найдите блок с opkg install. Замените его на следующий (мы добавили || true в конце, чтобы сборка не падала из-за ошибки rm):
RUN opkg update \
&& opkg install --force-overwrite --force-space \
luci-ssl \
uhttpd \
uhttpd-mod-ubus \
dnsmasq \
firewall4 \
nftables \
dropbear \
ip-full \
ca-bundle \
openssl-util \
&& rm -rf /var/opkg-lists \
&& (rm -rf /tmp/* 2>/dev/null || true)
Что делать дальше
Так как предыдущая сборка прервалась на этом шаге, Docker не сохранил этот слой. Вам нужно просто запустить сборку заново. Можно не использовать --no-cache для предыдущих шагов (распаковка архива уже закэширована и пройдет мгновенно), но сам opkg придется подождать еще раз:
docker compose build
После успешной сборки запустите контейнер:
docker compose up -d
Проверить, что контейнер жив и интерфейсы поднялись, можно командой:
docker exec -it openwrt ip -br addr
=> ERROR [openwrt openwrt 3/6] RUN echo «root:P@ssword2» | chpasswd
Ошибка возникает из-за того, что в стандартном rootfs OpenWrt утилита chpasswd отсутствует (она является частью пакета shadow, который не установлен по умолчанию). Docker пытается выполнить команду, не находит её и прерывает сборку.
Чтобы решить эту проблему надежно и не тянуть лишние пакеты, мы будем генерировать хэш пароля с помощью openssl (который мы уже установили в предыдущем шаге) и записывать его напрямую в файл /etc/shadow через sed.
Вам нужно внести изменения в два файла.
1. Обновите Dockerfile
Найдите в вашем Dockerfile блок установки пароля:
# Устанавливаем пароль root
RUN echo "root:${ROOT_PASSWORD}" | chpasswd
Замените его на следующий код:
# Устанавливаем пароль root через openssl и sed (так как chpasswd нет в базовом rootfs)
RUN HASH=$(openssl passwd -5 "${ROOT_PASSWORD}") && \
if grep -q '^root:' /etc/shadow; then \
sed -i "s|^root:[^:]*:|root:${HASH}:|" /etc/shadow; \
else \
echo "root:${HASH}:19800:0:99999:7:::" >> /etc/shadow; \
fi
2. Обновите entrypoint.sh
Такую же замену нужно сделать в скрипте инициализации, иначе контейнер упадет с той же ошибкой уже на этапе запуска (когда попытается переустановить пароль из переменных окружения).
Откройте файл entrypoint.sh, найдите этот блок:
if [ -n "${ROOT_PASSWORD:-}" ] && [ ! -f /etc/.docker_root_password_set ]; then
echo "root:${ROOT_PASSWORD}" | chpasswd
touch /etc/.docker_root_password_set
fi
Замените его на:
if [ -n "${ROOT_PASSWORD:-}" ] && [ ! -f /etc/.docker_root_password_set ]; then
HASH=$(openssl passwd -5 "${ROOT_PASSWORD}")
if grep -q '^root:' /etc/shadow; then
sed -i "s|^root:[^:]*:|root:${HASH}:|" /etc/shadow
else
echo "root:${HASH}:19800:0:99999:7:::" >> /etc/shadow
fi
touch /etc/.docker_root_password_set
fi
3. Пересоберите образ
Теперь запустите сборку заново. Так как мы изменили файлы, Docker корректно подхватит изменения:
# Остановите текущий контейнер, если он пытался запуститься
docker compose down
# Пересоберите образ
docker compose build
# Запустите
docker compose up -d
После успешного старта вы сможете зайти в Web-интерфейс OpenWrt (http://192.168.0.200 или http://192.168.200.1) под логином root и вашим паролем.