Создаем сеть:

docker network create vpn

Готовим образ с Wireguard:

FROM debian:trixie-slim@sha256:77ba0164de17b88dd0bf6cdc8f65569e6e5fa6cd256562998b62553134a00ef0

# wg-quick requirements: iproute2, procps
# envsubst util: gettext-base
RUN apt-get update && \
    apt-get install -y wireguard iproute2 procps gettext-base wget && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# Patch wg-quick if getting "sysctl: permission denied on key net.ipv4.conf.all.src_valid_mark" error.
#
# Credits: @kianbahasadri from (https://forums.docker.com/t/sysctl-error-setting-key-net-ipv4-conf-all-src-valid-mark-read-only-file-system/92567/10) 
# and https://github.com/linuxserver/docker-wireguard
#
# See also
# https://github.com/wg-easy/wg-easy/issues/2261
# and 
# https://github.com/WireGuard/wireguard-tools/pull/29/files
RUN sed -i 's|\[\[ $proto == -4 \]\] && cmd sysctl -q net\.ipv4\.conf\.all\.src_valid_mark=1|[[ $proto == -4 ]] \&\& [[ $(sysctl -n net.ipv4.conf.all.src_valid_mark) != 1 ]] \&\& cmd sysctl -q net.ipv4.conf.all.src_valid_mark=1|' /usr/bin/wg-quick

RUN mkdir /opt/app
WORKDIR /opt/app

RUN wget -O - https://github.com/DNSCrypt/dnscrypt-proxy/releases/download/2.1.15/dnscrypt-proxy-linux_x86_64-2.1.15.tar.gz | tar -xz && cp linux-x86_64/dnscrypt-proxy ./ && rm -r linux-x86_64
COPY ./dnscrypt-proxy.toml ./

COPY ./wg0.conf-template ./

Можно взять linuxserver/wireguard и не накладывать патч на wg-quick из-за ограничений в контейнере

name: vpn

services:
  wireguard:
    image: wireguard
    build: .
    container_name: vpn
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun:/dev/net/tun
    sysctls:
      # Patch wg-quick if getting "sysctl: permission denied on key net.ipv4.conf.all.src_valid_mark" error.
      #
      # Credits: @kianbahasadri from (https://forums.docker.com/t/sysctl-error-setting-key-net-ipv4-conf-all-src-valid-mark-read-only-file-system/92567/10) 
      # and https://github.com/linuxserver/docker-wireguard
      #
      # See also
      # https://github.com/wg-easy/wg-easy/issues/2261
      # and 
      # https://github.com/WireGuard/wireguard-tools/pull/29/files
      - net.ipv4.conf.all.src_valid_mark=1
    networks:
      - vpn
    dns:
      - 127.0.0.1
    command: bash -c "envsubst < wg0.conf-template > /etc/wireguard/wg0.conf && wg-quick up wg0 && ./dnscrypt-proxy"
    environment:
      # required
      - WG_ADDRESS=
      - WG_PRIVATEKEY=
      - WG_PEER_PUBLICKEY=
      - WG_PEER_ENDPOINT=

# Network creation: `docker network create vpn`
networks:
  vpn:
    external: true

Задаем нужные значения для WG_* переменных в docker-compose.yml

wg0.conf-template:

[Interface]
Address = ${WG_ADDRESS}
ListenPort = 51820
PrivateKey = ${WG_PRIVATEKEY}

[Peer]
PublicKey = ${WG_PEER_PUBLICKEY}
Endpoint = ${WG_PEER_ENDPOINT}
AllowedIPs = 0.0.0.0/0

Значения переменных заполняются через envsubst.

Собираем и запускаем:

docker compose build --progress plain wireguard

docker compose up -d wireguard

Допустим, есть приложение в контейнере (app), с отдельным Docker Compose файлом, которое нужно пустить в сеть через Wireguard и оно зависит еще от одного приложения (redis):

services:
  app:
    image: debian:trixie-slim@sha256:77ba0164de17b88dd0bf6cdc8f65569e6e5fa6cd256562998b62553134a00ef0
    network_mode: "container:vpn"
    depends_on:
      - redis
    
  redis:
    image: redis:8.4.0-bookworm@sha256:73dad4271642c5966db88db7a7585fae7cf10b685d1e48006f31e0294c29fdd7
    networks:
      - vpn

networks:
  vpn:
    external: true

Запускаем: docker compose up -d app

Проверяем, что приложение ходит по сети через тоннель: docker compose run --rm app bash -c "apt-get update && apt-get install -y curl && curl 'https://ident.me'"

Еще хорошо бы ��роверить (tcpdump + Wireshark), что Wireguard контейнер не шлет DNS запросы в обход Wireguard тоннеля.

Проверяем, что зависимость приложения (redis) доступна по имени сервиса: docker compose run --rm app bash -c "apt-get update && apt-get install -y redis-tools && redis-cli -h redis set a b"

Как это работает

network_mode: "container:vpn" помещает контейнер app в сеть контейнера wireguard. По документации не очень понятно, что кроме ID контейнера (который CONTAINER ID из docker ps) можно указывать и имя контейнера (NAMES из docker ps). Интересно, что docker inspect app-app-1 не показывает, что контейнер вообще состоит в какой-нибудь сети (пустые поля в "NetworkSettings").

container_name: vpn задает удобное короткое имя контейнера, чтобы не указывать wireguard-wireguard-1

Без добавления redis во внешнюю сеть vpn, в которой состоит и wireguard, app бы не смог найти redis по имени, ведь он видит по сети только то, что видит wireguard.

DNS запросы из app идут через wireguard, но без дополнительных настроек они бы шли далее через машину, где запущен контейнер wireguard. Один из способов это исправить - слать "DNS over HTTPS" запросы через тоннель. Для этого контейнер wireguard настроен (dns: - 127.0.0.1) на локальный DNSCrypt.

dnscrypt-proxy.toml:

server_names = ['static_cloudflare']

listen_addresses = ['127.0.0.1:53']

log_level = 5
use_syslog = true

ignore_system_dns = true

cache = true

[static]

[static.static_cloudflare]
  # See https://github.com/DNSCrypt/dnscrypt-resolvers/blob/master/v3/public-resolvers.md
  # and 
  # https://adguard-dns.io/kb/miscellaneous/create-dns-stamp/
  stamp = 'sdns://AgcAAAAAAAAABzEuMC4wLjEAEmRucy5jbG91ZGZsYXJlLmNvbQovZG5zLXF1ZXJ5'

DNSCrypt по-умолчанию берет DNS сервера из внешнего списка, который он запрашивает, используя системный DNS. Эти настройки указывают брать заданные адреса серверов (server_names) и не использовать системный DNS (ignore_system_dns). Для примера взят сервер CloudFlare в формате DNS stamp

У этого подхода есть большой минус: Контейнер app потеряет сеть, если wireguard контейнер перезагрузится. Способ вернуть app доступ в сеть через wireguard без перезагрузки app автору неизвестен.

P.S.

Слишком коротко для статьи, слишком длинно для поста.

Дополнение: Добавляем Wireguard к Docker образу без изменений в Dockerfile и docker-compose.yml.

Комментарии к статье (спасибо andy2000) подтолкнули к поиску другого решения: добавить Wireguard к образу, не меняя конфигурацию, и запускать контейнер из оригинального или дополненного образа. Чтобы дополнить Dockerfile можно было бы использовать INCLUDE+, но это нестандартное расширение команд Dockerfile. Philip Couling на StackOverflow предложил использовать buildx bake.

docker-compose.yml:

services:
  app:
    image: local-image:latest
    build:
      context: .
    command: sleep 3600
    depends_on:
      - redis
    
  redis:
    image: redis:8.4.0-bookworm@sha256:73dad4271642c5966db88db7a7585fae7cf10b685d1e48006f31e0294c29fdd7

docker-compose.vpn.yml:

services:
  app:
    image: local-image:vpn
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun:/dev/net/tun
    sysctls:
      # Patch wg-quick if getting "sysctl: permission denied on key net.ipv4.conf.all.src_valid_mark" error.
      #
      # Credits: @kianbahasadri from (https://forums.docker.com/t/sysctl-error-setting-key-net-ipv4-conf-all-src-valid-mark-read-only-file-system/92567/10) 
      # and https://github.com/linuxserver/docker-wireguard
      #
      # See also
      # https://github.com/wg-easy/wg-easy/issues/2261
      # and 
      # https://github.com/WireGuard/wireguard-tools/pull/29/files
      - net.ipv4.conf.all.src_valid_mark=1
    dns:
      - 127.0.0.1
    environment:
      - WG_ADDRESS=  # required. e.g. 10.0.0.2/24
      - WG_PRIVATEKEY=  # required
      - WG_PEER_PUBLICKEY=  # required
      - WG_PEER_ENDPOINT=  # required. e.g. 1.1.1.1:51821

Тут те же настройки для запуска Wireguard, как в предыдущем подходе, только другое имя образа - local-image:vpn.

Dockerfile.vpn:

# see docker-bake.hcl
FROM main-dockerfile

# wg-quick requirements: iproute2, procps
# envsubst util: gettext-base
RUN apt-get update && \
    apt-get install -y wireguard iproute2 procps gettext-base wget wait-for-it && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*
    
# Patch wg-quick if getting "sysctl: permission denied on key net.ipv4.conf.all.src_valid_mark" error.
#
# Credits: @kianbahasadri from (https://forums.docker.com/t/sysctl-error-setting-key-net-ipv4-conf-all-src-valid-mark-read-only-file-system/92567/10) 
# and https://github.com/linuxserver/docker-wireguard
#
# See also
# https://github.com/wg-easy/wg-easy/issues/2261
# and 
# https://github.com/WireGuard/wireguard-tools/pull/29/files
RUN sed -i 's|\[\[ $proto == -4 \]\] && cmd sysctl -q net\.ipv4\.conf\.all\.src_valid_mark=1|[[ $proto == -4 ]] \&\& [[ $(sysctl -n net.ipv4.conf.all.src_valid_mark) != 1 ]] \&\& cmd sysctl -q net.ipv4.conf.all.src_valid_mark=1|' /usr/bin/wg-quick

RUN mkdir /opt/app
WORKDIR /opt/app

RUN wget -O - https://github.com/DNSCrypt/dnscrypt-proxy/releases/download/2.1.15/dnscrypt-proxy-linux_x86_64-2.1.15.tar.gz | tar -xz && cp linux-x86_64/dnscrypt-proxy ./ && rm -r linux-x86_64
COPY ./dnscrypt-proxy.toml ./wg0.conf-template ./run.sh ./

RUN ./dnscrypt-proxy -service install

ENTRYPOINT ["./run.sh"]

run.sh

#!/usr/bin/env bash
set -e
envsubst < wg0.conf-template > /etc/wireguard/wg0.conf
wg-quick up wg0
./dnscrypt-proxy -service start
wait-for-it 127.0.0.1:53

exec "$@"

Тут сборка Wireguard, как в подходе выше, но ссылка на этап сборки main-dockerfile, который будет объявлен в docker-bake.hcl

docker-bake.hcl:

target "app-vpn" {
  tags = ["local-image:vpn"]
  dockerfile = "Dockerfile.vpn"
  contexts = {
    // Credits: Philip Couling
    // https://stackoverflow.com/questions/36362233/can-a-dockerfile-extend-another-one
    //
    // Target `app` will be automatically parsed from docker-compose.yml
    main-dockerfile = "target:app"
  }
}

Он позволяет ссылаться в Dockerfile.vpn на этап сборки main-dockerfile как на собранный образ app, даже если он еще не собран и без тега. Docker Bake посмотрит в docker-compose.yml, найдет там сервис app и запустит его сборку, если ее еще не было.

Обычная сборка и запуск приложения без Wireguard не изменятся: docker compose up app

Сборка приложения с Wireguard: docker buildx bake app-vpn

Запуск приложения с Wireguard: docker compose -f docker-compose.yml -f docker-compose.vpn.yml -f docker-compose.override.yml up app

Предполагается, что еще есть docker-compose.override.yml, где переопределены WG_* переменные.