Make Docker containers available both on your local network with Macvlan and on the web with Traefik

Sometime, you need to make a container accessible on your local network as if it were a device. But how to do this ?

Recently, I purchase a SSD drive for my Raspberry Pi, to replace the SD card. And since I have more space on the SSD, I have installed, with Docker, a lots of services, like OpenVPN, Pihole … and some media server like Emby or Jellyfin, or Navidrome. I was pretty surprise that my Pi can run some media server like Emby and play musics or movies with such a good quality. I can access to my Pi from the web and access to my media server with a reverse proxy : Traefik. It’s works great, but Emby or Jellyfin provide a DLNA server, so I can access to my files on my local network with a DLNA client, like my PS4. But it’s doesn’t work, the PS4 can’t access to the DLAN server in a Docker container, because they are not on the same network.

How could I make my Emby or Jellyfin server available both on the web to acces to my medias and on my local network to access to the DLNA functionalities ?

Network_mode : host

When you read the Jellyfin’s documentation to how install the server with docker, they provide a docker-compose.yml and suggest to use network_mode: host to to activate the DLNA functionnality.

version

:

"

3"

services

:

jellyfin

:

image

:

jellyfin/jellyfin

user

:

1000:1000

network_mode

:

"

host"

restart

:

"

unless-stopped"

volumes

:

-

/path/to/config:/config

-

/path/to/cache:/cache

-

/path/to/media:/media

The network_mode: host will bind all the ports of the host machine to the corresponding ports of the container. With this mode, the port 1900, wich is the port of the DLNA server, on the host machine will be automaticaly bind to the port 1900 of the container and the DLNA server will be available on the local network for all the DLNA client connected on the local network.

There is one drawback, you can’t mix network_mode and networks in a docker-compose.yml.It’s one or the other, but not both. If you don’t use networks, it’s the simpliest solution.
But if you have multiples containers, behind a reverse-proxy, like me, with Traeffik, you certainly have create a network to allow traefik to route your requests http to the correct container.

Macvlan driver

Despite the fact that the network_mode: host be an easy solution, Docker recommend to use the macvlan network mode instead of the host mode.

From the Docker documentation :

macvlan: Macvlan networks allow you to assign a MAC address to a container, making it appear as a physical device on your network. The Docker daemon routes traffic to containers by their MAC addresses. Using the macvlan driver is sometimes the best choice when dealing with legacy applications that expect to be directly connected to the physical network, rather than routed through the Docker host’s network stack.

Macvlan allow you to assign a MAC adress to a container, and the container will appear on the LAN Network. That’s great, that’s exactly what I want : make my container available for others devices on my local network.

So how to do that. You noticed it, I like to work with docker-compose,because it makes the use of containers easier than with command lines. It’s like a recipe that you can easily redo at will.

Check if Macvlan is available

Check on your host machine (in my case, a raspberry pi with raspbian) if the macvlan is availabe :
lsmod | grep macv

Find your ethernet interface

Use ifconfig -a to find your ethernet interface. eth0 or may be enp3s0 depending on your configuration.
Docker will associate your container with this interface.

Create the docker-compose.yml with a macvlan network

For my exemple, I will create an Emby server and a Jellyfin server (why choose…) But this will work for every containers you want to make available on your local network, like HomeAssistant, or Pihole…. whatever you want.

version

:

"

2.3"

services

:

emby

:

image

:

emby/embyserver:latest

container_name

:

emby

restart

:

"

unless-stopped"

volumes

:

-

./programdata:/config

-

./share:/mnt/share

devices

:

-

/dev/dri:/dev/dri

environment

:

-

UID=1000

# user id of the owner of the volumes on the host machine

-

GID=1000

# group id of the owner of the volumes on the host machine

networks

:

lan

:

# Define a static ip for the container. The containter can be accessible by others devices on the LAN network with this IP.

ipv4_address

:

192.168.1.155

jellyfin

:

image

:

jellyfin/jellyfin

container_name

:

jellyfin

user

:

1000:1000

restart

:

"

unless-stopped"

volumes

:

-

./config:/config

-

./cache:/cache

-

./share:/media

networks

:

lan

:

# Define a static ip for the container. The containter can be accessible by others devices on the LAN network with this IP.

ipv4_address

:

192.168.1.156

networks

:

lan

:

name

:

lan

driver

:

macvlan

driver_opts

:

parent

:

enp3s0

#your ethernet interface

ipam

:

config

:

-

subnet

:

192.168.1.0/16

# I use the same subnet as my LAN router.

I assume in the real life you don’t need to use emby and jellyfin at the same time. But just remove one or the other according to your need.

You need to create the volumes folders on the host machine before running the containers. I put my volumes folders at the same root that my docker-compose.yml. So, for Emby, two folders, one for the configuration and one for the medias to share.
And for Jellyfin, 3 folders, one for the config, one for the cache and one for the medias.

The two image need the user id and group id for the owner of the volumes on the host machine. Use this command at the root of your volumes ls -ln to find this infos.

The configuration for macvlan network are here :

networks

:

lan

:

name

:

lan

driver

:

macvlan

driver_opts

:

parent

:

enp3s0

#your ethernet interface

ipam

:

config

:

-

subnet

:

192.168.1.0/16

Note: this options only work with docker-compose version “2.*”

I create a network called lan, every container who will use this network will use the macvlan driver and will be associate to an interface specified in parent. In this case, the ethernet interface.

Inside ipam and config I can specify some options for the network. I want my container on the same local network that my others devices so in subnet I use the same subnet that my LAN router. You can also specify a gateway and a range of ip but I don’t need that.

I like to choose and specify a static IP adress for my container, so for each container, I specify the network to use and the IP I want for my container:

networks

:

lan

:

ipv4_address

:

192.168.1.156

So everything should work, the next step is to start the container.

Start the container

To start the container, use docker-compose up -d

If you run docker-compose ps you will see you containers running (with Ports empty) :

     Name              Command         State   Ports
----------------------------------------------------
emby              /init                Up           
jellyfin          /jellyfin/jellyfin   Up         

With docker network ls you will see all the Docker networks and their drivers :

docker network ls
NETWORK ID          NAME                         DRIVER              SCOPE
895d371d01ee        bridge                       bridge              local
6d8acfc1e42c        host                         host                local
3996cdb45860        lan                          macvlan             local

And you can inspect the macvlan network with : docker network inspect lan :

[
    {
        "Name": "lan",
        "Id": "3996cdb458606c3c04fd33cf6e8bf75b2f593752955a02004868675211f524a2",
        "Created": "2020-08-01T17:13:22.621770479+02:00",
        "Scope": "local",
        "Driver": "macvlan",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "192.168.1.0/16"
                }
            ]
        },
        "Internal": false,
        "Attachable": true,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "8cace653673ca0d2cf24a02725557053ec9d4a1d2d720b538f0157d8a3558c6a": {
                "Name": "jellyfin",
                "EndpointID": "42352c4517d5a7d3f8b017ca7558920c595ef4fd7ce346cbeaa02650261d3a91",
                "MacAddress": "02:42:c0:a8:01:a6",
                "IPv4Address": "192.168.1.156/16",
                "IPv6Address": ""
            },
            "db82ad8c16e029adffe43fe58fb64191b296835e3fb0bdb3d0d27a793ccef697": {
                "Name": "emby",
                "EndpointID": "71c6dcea6211d55c037aa4d5d81b0eb2628977c8e90d3e783e3f36a062da9585",
                "MacAddress": "02:42:c0:a8:01:a5",
                "IPv4Address": "192.168.1.155/16",
                "IPv6Address": ""
            }
        },
        "Options": {
            "parent": "enp3s0"
        },
        "Labels": {
            "com.docker.compose.network": "lan",
            "com.docker.compose.project": "emby",
            "com.docker.compose.version": "1.26.2"
        }
    }
]

And when I check to my router interface :

router
The containers are present on my LAN !

And when I try to access to this containers with the PS4 Media Player, my containers are showing up, like a classic DLNA server.
PS4 Media Player

Configure Traefik

Now I have this two containers available on my local network, but I want to access it from the web. And in this case, this two containers use the same ports.
I can use a reverse-proxy like Traefik to access to my container from the web and to handle the traffic on the same ports.

You need to have your own domain name.

First I need to create a specific network, I called it web : docker network create web
Every container who use this network would be managed by traeffik.

Traefik need a configuration file, called traefik.toml.
It should look like this.
For the web interface, you must replace the credentials by you own username and password. You also need to specify your own domain name.

debug

=

true

logLevel

=

"DEBUG"

defaultEntryPoints

=

["https","http"]

# API definition

# Warning: Enabling API will expose Traefik's configuration.

# It is not recommended in production,

# unless secured by authentication and authorizations

[api]

# Name of the related entry point

entryPoint

=

"traefik"

# Enable Dashboard

dashboard

=

true

# Redirect HTTPS (443) traffic to HTTP (80). This ports must be open on the modem/router

[entryPoints]

[entryPoints.http]

address

=

":80"

[entryPoints.http.redirect]

entryPoint

=

"https"

[entryPoints.https]

address

=

":443"

[entryPoints.https.tls]

#Secure the dashboard with a username & password

[entryPoints.traefik]

address

=

":8080"

[entryPoints.traefik.auth]

[entryPoints.traefik.auth.basic]

users

=

["username:password"]

# should look like this : users = ["foo:bar"]

[retry]

[docker]

endpoint

=

"unix:///var/run/docker.sock"

#Replace the domain name with your own domain.

domain

=

"www.my-domain.com"

watch

=

true

# Not expose container by default (to avoid "pollution")

exposedByDefault

=

false

usebindportip

=

true

[acme]

email

=

"[email protected]"

# Path to the acme.json into the traefik container (not the host) Permissions must be 0600

storage

=

"/etc/traefik/acme.json"

entryPoint

=

"https"

# For test and debugging, uncomment next line. Lets encrypt have rate limit, but there is no limit with the staging CA

# caServer = "https://acme-staging-v02.api.letsencrypt.org/directory"

# Create a certificate for each container (service) which have a traefik host rule defined

onHostRule

=

true

[acme.httpChallenge]

entryPoint

=

"http"

At the racine of the project, create the acme.json file: touch acme.json and change the permissions to 0600 : chmod 0600 acme.json

Now, I need to update the docker-compose.yml and add Traefik:

version

:

'

2.3'

services

:

traefik

:

image

:

traefik:alpine

container_name

:

traefik

ports

:

-

80:80

-

443:443

restart

:

"

always"

volumes

:

-

./traefik.toml:/etc/traefik/traefik.toml

-

./acme.json:/etc/traefik/acme.json

-

/var/run/docker.sock:/var/run/docker.sock

labels

:

-

"

traefik.enable=true"

-

"

traefik.docker.network=web"

-

"

traefik.port=8080"

-

"

traefik.backend=traefik"

-

"

traefik.frontend.rule=Host:traefik.my-domain.com"

networks

:

-

web

emby

:

image

:

emby/embyserver:latest

container_name

:

emby

restart

:

"

unless-stopped"

volumes

:

-

./programdata:/config

-

./share:/mnt/share

devices

:

-

/dev/dri:/dev/dri

environment

:

-

UID=1000

-

GID=1000

networks

:

lan

:

ipv4_address

:

192.168.1.155

web

:

labels

:

-

"

traefik.docker.network=web"

-

"

traefik.port=8096"

-

"

traefik.enable=true"

-

"

traefik.backend=emby"

-

"

traefik.frontend.rule=Host:emby.my-domain.com"

jellyfin

:

image

:

jellyfin/jellyfin

container_name

:

jellyfin

user

:

1000:1000

restart

:

"

unless-stopped"

volumes

:

-

./config:/config

-

./cache:/cache

-

./share:/media

networks

:

lan

:

ipv4_address

:

192.168.1.156

web

:

labels

:

-

"

traefik.docker.network=web"

-

"

traefik.port=8096"

-

"

traefik.enable=true"

-

"

traefik.backend=jellyfin"

-

"

traefik.frontend.rule=Host:jellyfin.my-domain.com"

networks

:

web

:

external

:

true

lan

:

name

:

lan

driver

:

macvlan

driver_opts

:

parent

:

enp3s0

#your ethernet interface

ipam

:

config

:

-

subnet

:

192.168.1.0/16

To start the container : docker-compose up -d

In your web browser, I you go to https://jellyfin.my-domain.com or https://emby.my-domain.com, you should access to your media serveur.

Conclusion

With this method, you can make your container available both on your local network, with macvlan and on the web with traefik and a domain name.

I chose to use containers corresponding to media servers, but you can use this method for any type of container. The most important being how you set up your macvlan network and how you set up traefik.