DEV Community

Cover image for Make Docker containers available both on your local network with Macvlan and on the web with Traefik
Fred Gauthier
Fred Gauthier

Posted on

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 = "foo@bar.com"
# 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.

Top comments (1)

Collapse
 
mexhigh profile image
Leon Schmidt

Thanks for your contribution. Maybe something that is interesting too: If you want to add a static mac address to the macvlan container, you can do so with this configuration:

version: '2.4'
services:
  some_service:
    image: some_image
    mac_address: 02:42:C0:A8:69:AC
    networks:
      lan:
        ipv4_address: 192.168.1.155
        priority: 1000
      web:
...
Enter fullscreen mode Exit fullscreen mode

The priority directive is only available in docker-compose file version 2, sadly. This prioritizes the macvlan network, as the mac address is only assigned to one of the network interfaces. Without the priority the mac address might be assigned to the web nic. The default priority is 0.