I encountered the problem getting no response in LLM chat using localai endpoint if http_proxy / https_proxy environment variables are set in the docker container.
The problem could be solve by clearing these env vars for n8n instance, but for other tasks in n8n I would like to use the proxy. If there’s a way to change the env vars for each task in n8n then it would be great, like setting from UI panel / fetching file from local, URL.
The provided solution using transparent proxy is not as complicated as if using docker compose. For now I managed to add a redsocks2 + mitmproxy instance to docker compose, n8n should use this instance as its default gateway.
For others reference I post my setup for n8n with transparent proxy here:
The docker compose file. Mitmproxy service should be build and up first, then build and run n8n service because mitmproxy would generate a CA certificate on the run which is needed by n8n during its build.
services:
n8n:
image: "my-n8n-image"
build:
context: .
dockerfile: Dockerfile
target: n8n
args:
- MITMPROXY_CA_CERT=./data/n8n/.mitmproxy/mitmproxy-ca-cert.pem
environment:
- NODE_ENV=production
env_file:
- .env
volumes:
- ./data/n8n/.n8n:/home/node/.n8n
- ./scripts/my_init.sh:/my_init.sh:ro
entrypoint: "/my_init.sh"
restart: "unless-stopped"
ports:
- 3000:3000
depends_on:
- mitmproxy
networks:
- default
cap_add:
- NET_ADMIN
mitmproxy:
image: "my-n8n-mitmproxy-image"
build:
context: .
dockerfile: Dockerfile
target: mitmproxy
entrypoint: "/mitmproxy_init.sh"
restart: "unless-stopped"
cap_add:
- NET_ADMIN
volumes:
- ./scripts/mitmproxy_init.sh:/mitmproxy_init.sh:ro
- ./data/n8n/.mitmproxy:/home/mitmproxy/.mitmproxy
my_init.sh
#!/bin/sh
python3 <<EOF
import time
import socket
import subprocess
import re
# get mitmproxy service ip address for "n8n-mitmproxy-1" using getaddrinfo
while True:
try:
info = socket.getaddrinfo("n8n-mitmproxy-1", 80)
if info is None:
time.sleep(1)
continue
break
except Exception:
time.sleep(1)
continue
proxy_ip = info[0][4][0]
# example: "default via 172.16.30.1 dev eth0"
re_default_gateway = re.compile(r"^default.*\bvia\s+(?P<ip>\d+\.\d+\.\d+\.\d+).*\bdev\s+(?P<iface>\w+)\b.*$")
gateway_ip = None
gateway_iface = None
for l in subprocess.run(["ip", "r"], capture_output=True).stdout.decode("utf-8").split("\n"):
m = re_default_gateway.search(l)
if m is None:
continue
gateway_ip = m.group("ip")
gateway_iface = m.group("iface")
break
print(f"{proxy_ip=}")
print(f"{gateway_ip=}, {gateway_iface=}")
if gateway_ip is not None:
subprocess.run(["ip", "r", "del", "default"])
subprocess.run(["ip", "r", "add", "default", "via", proxy_ip])
EOF
su node -c "/usr/bin/env -u HTTP_PROXY -u http_proxy -u HTTPS_PROXY -u https_proxy -u NO_PROXY -u NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/mitmproxy-ca-cert.pem TINI_SUBREAPER=true tini -- /docker-entrypoint.sh"
mitmproxy_init.sh
#!/bin/sh
REDSOCKS_PORT=${REDSOCKS_PORT:-21542}
MINIPROXY_PORT=${MINIPROXY_PORT:-1002}
MITMPROXY_PORT=8080
python3 <<EOF
import re
re_scheme = re.compile(r"(https?://)?(?P<ip_port>(\d+\.){3}\d+:\d+)")
UPSTREAM_PROXY="${HTTP_PROXY}" if len("${http_proxy}") == 0 else "${http_proxy}"
m = re_scheme.search(UPSTREAM_PROXY)
if m is not None:
UPSTREAM_PROXY = m.group("ip_port")
print(f"{UPSTREAM_PROXY=}")
UPSTREAM_BYPASS_DICT = dict()
UPSTREAM_BYPASS = list(filter(lambda h: len(h) > 0,
[h.strip() for h in "${NO_PROXY}".split(",")]))
for i, h in enumerate(UPSTREAM_BYPASS):
if h.startswith("*"):
UPSTREAM_BYPASS[i] = h[1:]
UPSTREAM_BYPASS_DICT[UPSTREAM_BYPASS[i]] = UPSTREAM_BYPASS[i]
with open("/tmp/tinyproxy.conf", "w") as f:
f.write("\n".join([
"Port ${MINIPROXY_PORT}",
"Upstream none \".localai_default\"",
]))
f.write("\n")
f.write("\n".join(map(lambda h: f"Upstream none \"{h}\"", UPSTREAM_BYPASS_DICT.keys())))
if len(UPSTREAM_PROXY) > 0:
f.write("\n")
f.write(f"Upstream http {UPSTREAM_PROXY}\n")
f.write("\n")
f.write("LogLevel Critical")
f.write("\n")
f.write("MaxClients 2000")
f.write("\n")
f.write("StartServers 10")
f.write("\n")
f.write("MaxSpareServers 10")
EOF
python3 <<EOF
cfg = """base {
log_debug = off;
log_info = off;
log = stderr;
daemon = off;
redirector = iptables;
reuseport = off;
}
redsocks {
bind = "0.0.0.0:${REDSOCKS_PORT}";
relay = "127.0.0.1:${MITMPROXY_PORT}";
type = direct;
}
"""
with open("/tmp/redsocks.conf", "w") as f:
f.write(cfg)
EOF
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port ${REDSOCKS_PORT}
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 443 -j REDIRECT --to-port ${REDSOCKS_PORT}
/usr/bin/env \
-u HTTP_PROXY \
-u http_proxy \
-u HTTPS_PROXY \
-u https_proxy \
-u NO_PROXY \
-u no_proxy \
tinyproxy -d -c /tmp/tinyproxy.conf &
redsocks2 -c /tmp/redsocks.conf &
docker-entrypoint.sh \
mitmdump \
--mode upstream:http://127.0.0.1:${MINIPROXY_PORT} \
--quiet
A custom docker image with python3.12 and iptables is needed
Dockerfile
FROM mitmproxy/mitmproxy as mitmproxy
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get -q update && \
DEBIAN_FRONTEND=noninteractive \
apt-get -q install --no-install-recommends -y \
tinyproxy iptables procps \
libevent-dev libssl-dev build-essential git
RUN git clone https://github.com/semigodking/redsocks.git /tmp/redsocks
RUN cd /tmp/redsocks/ && \
make distclean && \
make -j DISABLE_SHADOWSOCKS=true redsocks2 && \
cp /tmp/redsocks/redsocks2 /usr/local/bin/redsocks2 && \
rm -rf /tmp/redsocks/
FROM n8nio/n8n:latest as n8n
USER root
ARG MITMPROXY_CA_CERT
COPY ${MITMPROXY_CA_CERT} /usr/local/share/ca-certificates/mitmproxy-ca-cert.pem
RUN cat /usr/local/share/ca-certificates/mitmproxy-ca-cert.pem \
>> /etc/ssl/certs/ca-certificates.crt
RUN --mount=type=cache,target=/var/cache/apk \
&& apk update \
&& apk add --update --no-cache python3=~3.12 py3-pip curl iptables ca-certificates \
&& ln -sf python3 /usr/bin/python
COPY ${MITMPROXY_CA_CERT} /usr/local/share/ca-certificates/mitmproxy-ca-cert.pem
RUN update-ca-certificates