blog post hero image

HLS and DASH Webdav Streaming Server

| Oliver Rademaker

STREAMING DEVOPS DOCKER

Quellen und Nützliches

Docker

Dockerfile

FROM ubuntu:20.04 as base

ARG DEBIAN_FRONTEND=noninteractive
ENV TZ=Europe/Berlin

RUN apt-get update -y && apt-get upgrade -y && \
    apt-get install -y \
        nginx \
        nginx-extras \
        libnginx-mod-rtmp \
        libnginx-mod-http-geoip2 \
        libnginx-mod-http-geoip \
        libnginx-mod-http-dav-ext \
        libnginx-mod-http-dav-ext

RUN mkdir -p /var/log/nginx && \
    ln -sf /dev/stdout /var/log/nginx/access.log && \
    ln -sf /dev/stderr /var/log/nginx/error.log

CMD ["nginx", "-g", "daemon off;"]

Docker Stack Config

version: "3.8"

networks:
  cdn:
    driver: overlay
    attachable: true
  streaming_video:
    external: true

services:
  origin:
    image: localhost:5000/nginx-webdav:latest
    volumes:
      - "/opt/streaming-app/cdn/origin/nginx.conf:/etc/nginx/nginx.conf:ro"
      - "/opt/streaming-app/cdn/origin/conf.d:/etc/nginx/conf.d:ro"
      - "/mnt/gluster/vol/origin:/mnt/streaming-media:rw"
      - "/opt/streaming-app/cdn/cache/data:/usr/share/geoip:ro"
    networks:
      - cdn
      - streaming_video
    ports:
      - target: 8050
        published: 8050
        protocol: tcp
        mode: ingress
      - target: 8080
        published: 8080
        protocol: tcp
        mode: host
      # - target: 8090
      #   published: 8090
      #   protocol: tcp
      #   mode: ingress
    deploy:
      mode: replicated
      replicas: 3
      update_config:
        parallelism: 1
        delay: 1s
        order: stop-first
      placement:
        max_replicas_per_node: 1
        constraints: 
          - node.labels.origin == true
    logging:
      driver: grafana/loki-docker-driver
      options:
        loki-url: http://172.17.0.1:3100/loki/api/v1/push

  cache:
    image: localhost:5000/nginx-webdav:latest
    volumes:
      - "/opt/streaming-app/cdn/cache/nginx.conf:/etc/nginx/nginx.conf:ro"
      - "/opt/streaming-app/cdn/cache/conf.d:/etc/nginx/conf.d:ro"
      - "/opt/streaming-app/cdn/cache/data:/usr/share/geoip:ro"
      - "/opt/streaming-app/cdn/cache/ssl:/usr/share/ssl:ro"
    networks:
      - cdn
    ports:
      - target: 80
        published: 80
        protocol: tcp
        mode: host
      - target: 443
        published: 443
        protocol: tcp
        mode: host
      # - target: 8090
      #   published: 8090
      #   protocol: tcp
      #   mode: ingress
    deploy:
      mode: replicated
      replicas: 1
      update_config:
        parallelism: 1
        delay: 3s
        order: stop-first
      placement:
        max_replicas_per_node: 1
        constraints: 
          - node.labels.cache == true
    logging:
      driver: grafana/loki-docker-driver
      options:
        loki-url: http://172.17.0.1:3100/loki/api/v1/push

Origin-Server Konfiguration

nginx.conf

user  root;
worker_processes  1;

pid        /var/run/nginx.pid;

load_module "modules/ngx_http_geoip2_module.so";
load_module "modules/ngx_http_dav_ext_module.so";

events {
    worker_connections  1024;
}


http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout  65;

    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    # geoip
    geoip2 /usr/share/geoip/GeoLite2-Country.mmdb {
        $geoip2_data_country_code source=$remote_addr country iso_code;
        $geoip2_data_country_name source=$remote_addr country names en;
    }

    geoip2 /usr/share/geoip/GeoLite2-City.mmdb {
        $geoip2_data_city_name city names en;
    } 

    # logs
    log_format  geo  '$remote_addr - $remote_user [$time_local] "$request" '
                     '$status $body_bytes_sent "$http_referer" '
                     '"$http_user_agent" "$http_x_forwarded_for" '
                     '$geoip2_data_country_code $geoip2_data_country_name $geoip2_data_city_name';

    access_log /var/log/nginx/access.log geo;
    error_log /var/log/nginx/error.log warn;

    # gzip
    gzip on;
    gzip_vary on;

    # incude sub confs
    include /etc/nginx/conf.d/*.conf;
}

conf.d/webdav.conf

server {
    # LISTEN
    listen 8050;

    # WEB ROOT
    root /mnt/streaming-media;

    # do not show files
    autoindex on;

    location / {

        # WEBDAV
        dav_methods     PUT DELETE MKCOL COPY MOVE;
        dav_ext_methods PROPFIND OPTIONS;
        dav_access      user:rw group:rw all:rw;
        create_full_put_path on;	# allow to create sub directorys
        client_max_body_size 100M;

        # Limit Auth basic to Upload Files
        # limit_except GET PROPFIND OPTIONS HEAD {
        #     auth_basic "Access Restricted";
        #     auth_basic_user_file /etc/nginx/.webdav-origin.htpasswd;
        # }

        # Auto serve masterplaylists
        index master.mpd master.m3u8;

        # Disable cache
        add_header 'Cache-Control' 'no-cache';

        # CORS setup
        add_header "Access-Control-Allow-Origin" * always;
        add_header 'Access-Control-Expose-Headers' 'Content-Length';
        add_header "Access-Control-Allow-Methods" "GET, HEAD, POST, PUT, OPTIONS, MOVE, DELETE, COPY, LOCK, UNLOCK, PROPFIND" always;
        add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Requested-With, Content-Type, Accept, DNT, X-CustomHeader, Keep-Alive,User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Range, Range, Depth" always;


        # add some MIME types
        types {
            application/dash+xml mpd;
            application/vnd.apple.mpegurl m3u8;
        }

        # # allow CORS preflight requests
        # if ($request_method = 'OPTIONS') {
        #     add_header 'Access-Control-Allow-Origin' '*';
        #     add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        #     add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
        #     add_header 'Access-Control-Max-Age' 1728000;
        #     add_header 'Content-Type' 'text/plain; charset=utf-8';
        #     add_header 'Content-Length' 0;
        #     return 204;
        # }

        # if ($request_method = 'POST') {
        #     add_header 'Access-Control-Allow-Origin' '*';
        #     add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        #     add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
        #     add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
        # }

        # if ($request_method = 'GET') {
        #     add_header 'Access-Control-Allow-Origin' '*';
        #     add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        #     add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
        #     add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
        # }
    }
}

conf.d/origin.conf

server {
	listen 8080;
	
	# Serve HLS fragments
	location / {
		types {
			application/vnd.apple.mpegurl m3u8;
			video/mp2t ts;
		}

		gzip_types application/vnd.apple.mpegurl;
		
		root /mnt/streaming-media;

		# Disable cache
		add_header Cache-Control no-cache;
		
		# CORS setup
		add_header Access-Control-Allow-Origin * always;
		# add_header Access-Control-Expose-Headers Content-Length;
	}
}

conf.d/stats.conf

server {
    listen 8090;

    allow 192.168.0.0/16;
    deny  all;

    location /stats {
        stub_status;
    }
}

Cache-Server Konfiguration

nginx.conf

user root;
worker_processes  1;

load_module "modules/ngx_http_geoip2_module.so";

events {
    worker_connections 1048576;
    multi_accept on;
    use epoll;
}

http {
    # BASIC
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    server_tokens off;
    keepalive_timeout 300s;
    types_hash_max_size 2048;
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # GEOIP
    geoip2 /usr/share/geoip/GeoLite2-Country.mmdb {
        $geoip2_data_country_code source=$remote_addr country iso_code;
        $geoip2_data_country_name source=$remote_addr country names en;
    }

    geoip2 /usr/share/geoip/GeoLite2-City.mmdb {
        $geoip2_data_city_name  city names en;
        $geoip2_data_latitude   location latitude;
        $geoip2_data_longitude  location longitude;
    } 

    # LOGS
    log_format json_analytics escape=json '{'
                            '"msec": "$msec", ' # request unixtime in seconds with a milliseconds resolution
                            '"connection": "$connection", ' # connection serial number
                            '"connection_requests": "$connection_requests", ' # number of requests made in connection
                    '"pid": "$pid", ' # process pid
                    '"request_id": "$request_id", ' # the unique request id
                    '"request_length": "$request_length", ' # request length (including headers and body)
                    '"remote_addr": "$remote_addr", ' # client IP
                    '"remote_user": "$remote_user", ' # client HTTP username
                    '"remote_port": "$remote_port", ' # client port
                    '"time_local": "$time_local", '
                    '"time_iso8601": "$time_iso8601", ' # local time in the ISO 8601 standard format
                    '"request": "$request", ' # full path no arguments if the request
                    '"request_uri": "$request_uri", ' # full path and arguments if the request
                    '"args": "$args", ' # args
                    '"status": "$status", ' # response status code
                    '"body_bytes_sent": "$body_bytes_sent", ' # the number of body bytes exclude headers sent to a client
                    '"bytes_sent": "$bytes_sent", ' # the number of bytes sent to a client
                    '"http_referer": "$http_referer", ' # HTTP referer
                    '"http_user_agent": "$http_user_agent", ' # user agent
                    '"http_x_forwarded_for": "$http_x_forwarded_for", ' # http_x_forwarded_for
                    '"http_host": "$http_host", ' # the request Host: header
                    '"server_name": "$server_name", ' # the name of the vhost serving the request
                    '"request_time": "$request_time", ' # request processing time in seconds with msec resolution
                    '"upstream": "$upstream_addr", ' # upstream backend server for proxied requests
                    '"upstream_connect_time": "$upstream_connect_time", ' # upstream handshake time incl. TLS
                    '"upstream_header_time": "$upstream_header_time", ' # time spent receiving upstream headers
                    '"upstream_response_time": "$upstream_response_time", ' # time spend receiving upstream body
                    '"upstream_response_length": "$upstream_response_length", ' # upstream response length
                    '"upstream_cache_status": "$upstream_cache_status", ' # cache HIT/MISS where applicable
                    '"ssl_protocol": "$ssl_protocol", ' # TLS protocol
                    '"ssl_cipher": "$ssl_cipher", ' # TLS cipher
                    '"scheme": "$scheme", ' # http or https
                    '"request_method": "$request_method", ' # request method
                    '"server_protocol": "$server_protocol", ' # request protocol, like HTTP/1.1 or HTTP/2.0
                    '"pipe": "$pipe", ' # "p" if request was pipelined, "." otherwise
                    '"gzip_ratio": "$gzip_ratio", '
                    '"http_cf_ray": "$http_cf_ray",'
                    '"geoip_country_code": "$geoip2_data_country_code",'
                    '"geoip_country_name": "$geoip2_data_country_name",'
                    '"geoip_city_name": "$geoip2_data_city_name",'
                    '"geoip_longitude": "$geoip2_data_longitude",'
                    '"geoip_latitude": "$geoip2_data_latitude",'
                    '"geoip_location": {"target": "$geoip2_data_country_code", "datapoints": [[$geoip2_data_longitude, $geoip2_data_latitude]]}'
                    '}';

    access_log /var/log/nginx/access.log json_analytics;
    error_log /var/log/nginx/error.log warn;

    # gzip
    gzip on;
    gzip_disable "msie6";
    gzip_http_version 1.1;
    gzip_comp_level 6;
    gzip_types text/plain text/css application/json application/javascript text/javascript application/x-javascript text/xml application/xml application/xml+rss application/vnd.ms-fontobject application/x-font-ttf font/opentype font/x-woff image/svg+xml image/x-icon;

    # proxy
    proxy_redirect off;
    proxy_http_version 1.1;
    proxy_read_timeout 10s;
    proxy_send_timeout 10s;
    proxy_connect_timeout 10s;
    proxy_cache_path /http-cache-data use_temp_path=off keys_zone=origin_cache_temp:1m max_size=1g inactive=5m;
    proxy_cache origin_cache_temp;
    proxy_cache_methods GET HEAD;
    proxy_cache_key $uri;
    proxy_cache_valid 200 302 1m;
    proxy_cache_valid 404 3s;
    proxy_cache_lock on;
    proxy_cache_lock_age 5s;
    proxy_cache_lock_timeout 1h;
    proxy_ignore_headers Cache-Control;
    proxy_ignore_headers Set-Cookie;

    # sendet die IP-Adresse des Clients zum Origin
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-forwarded-host $host;

    # incude sub confs
    include /etc/nginx/conf.d/*.conf;
}

conf.d/cache.conf

server {
    resolver 127.0.0.11;
    set $originService "cdn_origin";

    listen 80;
    # listen 443 ssl;
    # server_name         example.com;
    # ssl_certificate     /usr/share/ssl/fullchain.pem;
    # ssl_certificate_key /usr/share/ssl/privkey.pem;

    add_header X-Cache-Status $upstream_cache_status;

    location ~* \.(m3u8|mpd)$ {
        proxy_cache_valid 200 302 4s;
        # proxy_pass http://origins;
        proxy_pass http://$originService:8080;
    }

    location / {
        # proxy_pass http://origins;
        proxy_pass http://$originService:8080;
    }
}

conf.d/stats.conf

server {
    listen 8090;

    allow 192.168.0.0/16;
    deny  all;

    location /stats {
        stub_status;
    }
}