Dedicated IP with macvlan
macvlan networking gives a Kappa Graph deployment its own MAC address and LAN IP, making the platform appear as a distinct host on your network. Use this when ports 80/443 on the Docker host are already in use, or when you want the service reachable at a stable LAN address without a reverse proxy in front of it.
When to use this
- The Docker host runs Traefik, Caddy, or another service that already owns ports 80/443.
- You want
https://kg.example.comto resolve directly to the container, not through the host. - Multiple services need port 443 simultaneously on the same physical machine.
How it works
┌──────────────────────────────────────────────────────────────┐
│ Physical LAN │
│ 192.168.1.0/24 │
├──────────────────────────────────────────────────────────────┤
│ │
│ Docker Host Kappa Graph │
│ 192.168.1.10 192.168.1.82 │
│ ├── traefik :80/443 └── kg-traefik :80/443 │
│ └── other services (TLS terminated by Traefik) │
│ │
│ Both appear as separate hosts on the LAN. │
└──────────────────────────────────────────────────────────────┘
The macvlan driver creates a virtual network interface backed by your physical NIC. The container gets its own MAC address and IP; the host kernel bridges traffic at layer 2.
Limitation: macvlan containers cannot communicate with the Docker host over the same interface. Access the platform from any other device on the LAN. See Host-to-container routing if you need the host to reach the container.
Prerequisites
- Linux host with Docker installed
- A physical NIC with a known interface name (
eno1,eth0, etc.) - A static IP address outside your DHCP range, or a DHCP reservation
- A DNS A record pointing your hostname to that static IP
- A domain with a DNS provider that supports DNS-01 challenge if the host is behind NAT (Porkbun, Cloudflare, and ~100 others are supported)
Network planning
Decide these values before running any commands:
| Item | Example | Notes |
|---|---|---|
| Parent interface | eno1 |
Run ip link show to find yours |
| Subnet | 192.168.1.0/24 |
Your LAN subnet |
| Gateway | 192.168.1.1 |
Your router |
| Static IP | 192.168.1.82 |
Must be outside DHCP range |
| Hostname | kg.example.com |
DNS A record pointing to the static IP |
Use a static IP, not DHCP. Docker generates a random MAC for each container on secondary networks, so DHCP reservations tied to a MAC address will not get a consistent assignment.
Step 1 — Create the macvlan network
docker network create -d macvlan \
--subnet=192.168.1.0/24 \
--gateway=192.168.1.1 \
-o parent=eno1 \
lannet
Adjust --subnet, --gateway, and -o parent for your network. The network name lannet matches the external: true entry in docker/docker-compose.prod.yml.
Step 2 — Configure the static IP
docker/docker-compose.prod.yml already includes the lannet macvlan attachment for the web service with a placeholder IP. Edit it to match your planned static IP:
# docker/docker-compose.prod.yml (excerpt)
services:
web:
networks:
kg-internal:
lannet:
ipv4_address: 192.168.1.82 # ← your static IP
networks:
lannet:
external: true
No other compose file needs editing; the ports: mappings in the base docker-compose.yml are irrelevant once the container has a direct LAN IP.
Step 3 — Enable TLS with Traefik
Kappa Graph uses Traefik as its internal router (ADR-105). Set ROUTER_MODE and TLS_MODE in your .operator.conf file (located in the project root):
# .operator.conf
ROUTER_MODE=traefik
TLS_MODE=letsencrypt
LE_EMAIL=admin@example.com
EXTERNAL_URL=https://kg.example.com
operator.sh reads this file at startup and selects the matching compose overlays:
TLS_MODE |
Overlays added | Effect |
|---|---|---|
none |
(none) | HTTP only, no redirect |
selfsigned |
traefik-tls.yml |
HTTPS with Traefik's built-in self-signed cert |
letsencrypt |
traefik-tls.yml + traefik-tls-letsencrypt.yml |
HTTPS with auto-renewed Let's Encrypt cert (TLS-ALPN-01) |
manual |
traefik-tls.yml + traefik-tls-manual.yml |
HTTPS with a certificate you supply |
For TLS-ALPN-01 (letsencrypt mode), port 443 must be reachable from the public internet. If the host is behind NAT, use DNS-01 instead — see DNS-01 challenge for NAT hosts.
Step 4 — Create the DNS record
Point your hostname to the static IP at your DNS provider:
| Type | Name | Value | TTL |
|---|---|---|---|
| A | kg | 192.168.1.82 | 300 |
For kg.example.com, enter kg as the record name under the example.com zone. For local-only access, add an entry to /etc/hosts or your local DNS resolver instead.
Step 5 — Start the platform
Traefik obtains the certificate on first startup and stores it under ./acme/acme.json (or the path set by KG_ACME_DIR). Renewal is automatic.
Verify the deployment
Run these checks from a device other than the Docker host:
# Check container health
./operator.sh status
# Confirm macvlan IP assignment
docker inspect kg-web \
--format '{{range $k, $v := .NetworkSettings.Networks}}{{$k}}: {{$v.IPAddress}}{{"\n"}}{{end}}'
# Test HTTPS from another LAN host
curl -s https://kg.example.com/health
The health endpoint returns healthy when the platform is up.
DNS-01 challenge for NAT hosts
When port 443 is not reachable from the public internet, TLS-ALPN-01 cannot complete. Use DNS-01 instead: the challenge is answered by adding a DNS TXT record, so no inbound port is required.
DNS-01 is opt-in. It requires placing DNS provider credentials in the Traefik container environment. Edit docker/docker-compose.traefik-tls-letsencrypt.yml to switch the resolver from tlschallenge to dnschallenge:
# In the traefik service command, replace:
- --certificatesresolvers.le.acme.tlschallenge=true
# with:
- --certificatesresolvers.le.acme.dnschallenge=true
- --certificatesresolvers.le.acme.dnschallenge.provider=porkbun
Then pass the provider credentials via environment variables in the same overlay:
services:
traefik:
environment:
PORKBUN_API_KEY: ${PORKBUN_API_KEY}
PORKBUN_SECRET_API_KEY: ${PORKBUN_SECRET_API_KEY}
Set those variables in .env before starting. Provider names and their expected environment variables are listed in the lego DNS provider docs.
For hosts that prefer to issue certificates off-box, use TLS_MODE=manual and mount the cert files — see TLS and Certificates.
Host-to-container routing
macvlan containers are isolated from the Docker host at layer 2. If you need the host to reach the container (for example, to run curl health checks from the host itself), add a macvlan interface on the host:
# Create a host-side macvlan interface on an adjacent IP
ip link add macvlan0 link eno1 type macvlan mode bridge
ip addr add 192.168.1.83/32 dev macvlan0
ip link set macvlan0 up
# Route traffic to the container IP through it
ip route add 192.168.1.82/32 dev macvlan0
Replace 192.168.1.83 with any unused IP adjacent to your container's address. This route is not persistent across reboots — add it to your network manager or a systemd unit if you need it permanently.
Troubleshooting
Container gets the wrong IP or no IP
Static assignment via ipv4_address in the compose file is the reliable path. If the container is using DHCP (no ipv4_address set), Docker's randomly generated MAC will not match any DHCP reservation. Set the IP statically in docker-compose.prod.yml and recreate the container:
Certificate is not issuing
Check Traefik logs for ACME errors:
For DNS-01: confirm the provider credentials are set and the DNS TXT record appears before the validation timeout. Use Let's Encrypt's staging CA while testing to avoid rate limits — set LE_CASERVER=https://acme-staging-v02.api.letsencrypt.org/directory in .operator.conf.
Host cannot reach the container
Expected behavior with macvlan. Add a host-side macvlan interface as described above, or test from another device on the LAN.
Container won't start
The most common cause is an IP conflict — another device on the LAN already holds 192.168.1.82. Confirm the IP is free before assigning it.