Make Docker containers available both on your local network with Macvlan and on the web with Traefik
Tags: docker, macvlan, traefik, emby
Published: 2020-08-01 16:40:10

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](https://jellyfin.org/) or [Jellyfin](https://jellyfin.org/), 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](https://en.wikipedia.org/wiki/Digital_Living_Network_Alliance), 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](https://jellyfin.org/docs/general/administration/installing.html) 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.

```yaml
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](https://docs.docker.com/network/) :
>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.

```yaml
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 :
```yaml
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:
```yaml
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](https://i.ibb.co/58JNxND/Capture-d-cran-du-2020-08-01-17-19-43.png)
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](https://i.ibb.co/4t1WPvq/DSC-0072.jpg)

#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](https://docs.traefik.io/) 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.*

```toml
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:

```yaml
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.