English version - README.en.md
Docker + Django + Nginx + uWSGI + Postgres 基本教學 - 從無到有
教你用 Docker 建立 Django + Nginx + uWSGI + PostgreSQL 📝
建議對 Docker 還不熟的朋友,可以先參考我之前寫的
Docker 基本教學 - 從無到有 Docker-Beginners-Guide 教你用 Docker 建立 Django + PostgreSQL 📝
-
Youtube Tutorial PART 1 - Docker + Django + Nginx + uWSGI + Postgres - 簡介
-
Youtube Tutorial PART 2 - Docker + Django + Nginx + uWSGI + Postgres - 原理步驟
-
Youtube Tutorial PART 3 - Docker + Django + Nginx + uWSGI + Postgres - 實戰
之前介紹過了,這邊就不在介紹 😝
請參考
Docker 基本教學 - 從無到有 Docker-Beginners-Guide 教你用 Docker 建立 Django + PostgreSQL 📝
請參考
Django 基本教學 - 從無到有 Django-Beginners-Guide 📝
Django-REST-framework 基本教學 - 從無到有 DRF-Beginners-Guide 📝
更多 Django 的範例可以參考我的 Github,這裡我就列出比較基本的兩篇就好:relaxed:
Nginx 是一種 Web Server,使用資源少且穩定性高,穩定性高這部分可參考
Nginx 解決了 C10K 問題,什麼是 C10K? 原文可參考 The C10K problem ,
C10K 就是 Client 10000 的問題,在過去,如果同時連接到 Server 的 Client
端數超過 10000 中,可能無法正常提供服務。
Ngnix 本身沒辦法處理動態內容,所以必須另外設定 uWSGI 來處理之間的互動
,參考下方流程 ( 重要 )
⭐ the web client <-> the web server ( Nginx ) <-> unix socket <-> uWSGI <-> Django ⭐
你可能會問我,uWSGI 這個是什麼 😕
uWSGI 是一種通信協議,可以把它想成是一種接口 ( 和 Django 進行溝通 ),
通常 Django 程式會放在 http server( Nginx )上,那當 server 接收到
request 時,該怎麼將這些數據傳遞(轉換)給 Django 呢 ?
這就是 uWSGI 的功能 😉
那為什麼還需要 Nginx 呢 😕
先了解一個觀念,
Nginx 負責靜態內容(html js css 圖片...... ),uWSGI 負責 Python 的動態內容。
uWSGI 對於靜態內容處理的並不是很好( 效能差 ),所以我們可以透過
Nginx 來處理靜態內容,而且使用 Nginx 還有很多好處,
- Nginx 比起 uWSGI 能更好地處理靜態資源
- Nginx 可以設定 Cache 機制
- Nginx 可以設定 反向代理器
- Nginx 可以進行多台機器的負載平衡( Load balance )
溫馨小提醒:heart:
如果你想更進一步的了解反向代理器,可參考 正向代理器 VS 反向代理器 的說明 😄
看到這邊你可能會問我?
那為什麼我在本機開發時,都不需要有 Nginx 以及 uWSGI 就可以執行呢 😕
當你在開發 Django 時,我們通常都是用 python manage.py runserver
去執行,當你執行這段指令時,其實他就是幫你建立一個小型的 http server ,
當然,這只是開發方便,正式環境是不會這樣使用的( 更何況效能的部份 😥 )
疑 😕 好像有 Gunicorn 這個東西,之前有講過 Gunicorn ,可參考
Deploying_Django_To_Heroku_Tutorial Deploying-Flask-To-Heroku
那為什麼不用 Gunicorn,要用 uWSGI 呢?
那時候會使用 Gunicorn,是因為在 Heroku 裡,官方建議使用 Gunicorn 來
啟動 web server,至於 Gunicorn 和 uWSGI 哪個比較好,我覺得要依照自
己的使用情境下去選擇 😉
等等,既然都講到了 Nginx,不是還有 Apache,聽說那個好像很多人在用 😜
我認為沒有哪個 Server 最好,重點在你的需求下,哪個 Server 最符合你的情境(需求),
你就選他 😃
這次我將利用 Docker 建立 3 個容器( Containers ),分別為 Nginx、Django + uWSGI 、Postgres
我主要是參考 https://uwsgi-docs.readthedocs.io/en/latest/tutorials/Django_and_nginx.html 這篇教學,
但些許部分不太一樣 😏
這次的重點會放在 Nginx 以及 Django + uWSGI 設定的部份,
Nginx 的部份,可參考 Nginx 資夾中的 Dockerfile
FROM nginx:latest
COPY nginx.conf /etc/nginx/nginx.conf
COPY my_nginx.conf /etc/nginx/sites-available/
RUN mkdir -p /etc/nginx/sites-enabled/\
&& ln -s /etc/nginx/sites-available/my_nginx.conf /etc/nginx/sites-enabled/
# RUN mkdir -p /etc/nginx/sites-enabled/\
# && ln -s /etc/nginx/sites-available/my_nginx.conf /etc/nginx/sites-enabled/\
# && rm /etc/nginx/conf.d/default.conf
CMD ["nginx", "-g", "daemon off;"]
解釋一下裡面的步驟,
第一步
先將 nginx.conf 複製到 /etc/nginx/nginx.conf
的路徑,
( 原始的 nginx.conf 可以從 Docker 的 Nginx 容器中取得,在 /etc/nginx
路徑下取得 nginx.conf )
我有複製一份原始的出來 nginx_origin.conf 😃
nginx.conf 主要是修改兩個部分,
一部分是將 user 從 nginx 修改為 root,
user root;
另一部份是
# include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-available/*;
增加一行 include /etc/nginx/sites-available/*;
並且將 include /etc/nginx/conf.d/*.conf;
這行註解掉,
這樣在 Nginx 資料夾中的 Dockerfile 就不用再執行刪除 default.conf 的指令,
因為 include /etc/nginx/conf.d/*.conf;
它是默認會跑的頁面,
但我們現在要設定自己的 😏
第二步
將 my_nginx.conf 複製到 /etc/nginx/sites-available/
裡面,
這邊我們先暫停一下,
如果你是用 FROM nginx:latest
的方式安裝 Nginx,你會發現你沒有以下兩個路徑
/etc/nginx/sites-available/
/etc/nginx/sites-enabled/
但不要擔心,沒有我們就自己建立 ( 也就是 Nginx 資料夾中的 Dockerfile 所執行的指令 ) ,
但為什麼我們沒有這些路徑呢 😕
原因是這些默認路徑似乎是要用 apt-get
的方式安裝 Nginx 才會有默認的路徑。
第三步
sites-available 這個資料夾其實不重要,你也可以取名自己喜歡的資料夾,但
sites-enabled 這個資料夾就比較重要了,因為我們要使用 Symlink 的方法
(透過 Linux 中的 ln
指令 )將 sites-enabled 以及 my_nginx.conf 連結起來。
接著來說說 my_nginx.conf 裡面的設定
# the upstream component nginx needs to connect to
upstream uwsgi {
# server api:8001; # use TCP
server unix:/docker_api/app.sock; # for a file socket
}
# configuration of the server
server {
# the port your site will be served on
listen 80;
# index index.html;
# the domain name it will serve for
# substitute your machine's IP address or FQDN
server_name twtrubiks.com www.twtrubiks.com;
charset utf-8;
client_max_body_size 75M; # adjust to taste
# Django media
# location /media {
# alias /docker_api/static/media; # your Django project's media files - amend as required
# }
location /static {
alias /docker_api/static; # your Django project's static files - amend as required
}
location / {
uwsgi_pass uwsgi;
include /etc/nginx/uwsgi_params; # the uwsgi_params file you installed
}
}
也可以透過以下的指令初步測試 nginx 是否設定正確,
nginx -t
# or
nginx -T
-t
代表 test configuration and exit.
-T
代表 test configuration, dump it and exit.
先來看 upstream 的部份,使用 Unix sockets 的方式,
會比使用 TCP port socket 的方式還要好,因為開銷比較小。
再來是 include /etc/nginx/uwsgi_params
的部份,一般來說,
Nginx 的路徑 /etc/nginx/
底下可以找的到 uwsgi_params,如果
真的找不到,可以到這裡複製進去 uwsgi_params。
(我複製出來一份給大家,如果你照按我的步驟基本上都是會有)
再來需要說明一下 uwsgi_pass 這個東西,或是說你可能看過的是 proxy_pass,
Nginx 會把接收到的 request 依照 uwsgi 協議轉換,然後再轉發給 Django 處理,
那為什麼不使用 proxy( 預設是 http 協議 )就好,要特別使用 uwsgi 😕
主要是效能上的考量。
既然都講到了這邊了,簡單解釋一下什麼是 Proxy server,
一般稱為代理伺服器,當外部網路的使用者送出一個 request 時,Proxy server 會
將這個 request 轉送到內部網路的 server 上處理,處理完之後,再透過 Proxy server
將 resopnse 回傳給外部網路的使用者。
這樣做有什麼好處呢 😕 好處是可以保護內部 server 的安全,避免使用者可以直接
對 server 進行攻擊,其他好處像是還有可以做 Cache 機制,如果使用者再次存取相同的資
料,就可以直接從 Cache 中取出。
最後一步,
溫馨小提醒:heart:
什麼是 daemon ❓❓❓
其實不用把他想的太難,簡單把他想成是一種 service 即可:smile:
如果想更深入的了解 daemon,請 google linux daemon ✏️
為什麼要使用 nginx -g daemon off
的方式啟動 Nginx,
而不用一般的 /etc/init.d/nginx start
方式啟動呢 😕
這問題就必須回到 Docker 中去瞭解,
以下為 Docker Nginx 的說明
If you add a custom CMD in the Dockerfile, be sure to include -g daemon off; in the CMD in order for nginx to stay in the foreground, so that Docker can track the process properly (otherwise your container will stop immediately after starting)!
簡單來說,就是要讓 Nginx 一直保持服務,否則 Container 會退出並且停止。
Django + uWSGI 的部份,可參考 api 資料夾裡面的 Dockerfile,
裡面基本上很簡單,但有一個想提一下,有時候我們
pip install
的時候很慢,
這時候可以考慮加個 -i
去改變它的來源,讓下載快一點 😁
接著說明 uwsgi.ini,裡面是一些設定檔
[uwsgi]
# http=0.0.0.0:8000
socket=app.sock
master=true
# maximum number of worker processes
processes=4
threads=2
# Django's wsgi file
module=django_rest_framework_tutorial.wsgi:application
# chmod-socket=664
# uid=www-data
# gid=www-data
# clear environment on exit
vacuum = true
透過 socket file ( app.sock ) 和 Nginx 溝通,uid 和 gid 則是權限的部份,
可參考下面這篇文章說明,裡面有提到不要用 root 權限,
Things to know (best practices and 「issues」) READ IT !!!
我最後還是選擇使用 root 下去執行,原因是如果沒有使用 root ,會出現權限錯誤,
最後我在 這邊 找到答案,
the socket API bind() to a port less than 1024, such as 80 as your title mentioned, need root access.
比較簡單的解法就是使用 root 執行:smile:
最後就是使用 docker-compose.yml
管理這些 Container 了,
可直接參考 docker-compose.yml
直接執行 docker-compose up
見證奇蹟
你會看到類似的圖
如果再看到類似下圖就代表成功了
接著瀏覽 http://localhost/
如果你順利看到以下畫面代表成功一小步,
接著再瀏覽 http://localhost/api/musics/
看到這些都是正常的,因為我們還必須 migrate。
terminal 輸出也沒有任何問題 ( 雖然這裡很容易卡關 😅 )
接著開啟另一個 terminal,進入 api ( Django + uWSGI ) 的容器,
指令可參考之前的 docker-tutorial-指令介紹,
也可以使用其他 GUI 工具 之前介紹的 portainer
docker exec -it <Container ID> bash
python manage.py makemigrations musics
python manage.py migrate
python manage.py createsuperuser
這次我們要多執行一個指令
python manage.py collectstatic
將 Django 中的 static files 收集起來,變成 static folder
接著可以再瀏覽 http://localhost/api/musics/,
你就會發現正常顯示了 😄
為什麼我們要執行這步驟?
主要是把這些靜態內容交給 Nginx 去處理,在 my_nginx.conf 中,
可以發現我們將路徑指到 /docker_api/static
。
前面有說過,
Nginx 負責靜態內容( html css 圖片...... ),uWSGI 負責 Python 的動態內容。
如果你有興趣可以試試看,使用 Django + uWSGI 不使用 Nginx,這樣依然
可以正常執行,但你會發現你的 css 圖片 那些全部會抓不到,如下圖,
因為 uWSGI 本身對處理靜態的內容不是很好 😭
雖然可以解決,可參考 https://uwsgi-docs.readthedocs.io/en/latest/StaticFiles.html,
但建議使用 Nginx,可以做的事情也比較多:smiley:
瀏覽 http://localhost/api/musics/
打開 stub_status 模組, 請參考 my_nginx.conf,
location /nginx/status {
# 啟用 stub_status
stub_status on;
# 關閉/啟用 log
# access_log /usr/local/nginx/logs/status.log;
access_log off;
auth_basic "NginxStatus";
# 限制可存取的 IP
# allow 127.0.0.1;
# deny all;
}
目前 nginx 的連接狀況
也可以設定只允許哪些 ip 訪問此頁面, 如果不在 ip 內會出現拒絕403
主要加入 auth_basic
和 auth_basic_user_file
文件可參考 Module ngx_http_auth_basic_module
location /nginx/status {
# 啟用 stub_status
stub_status on;
# 關閉/啟用 log
# access_log /usr/local/nginx/logs/status.log;
access_log off;
auth_basic "NginxStatus";
auth_basic_user_file /my_htpasswd/htpasswd;
# 限制可存取的 IP
# allow 127.0.0.1;
# deny all;
}
建立一個 htpasswd 檔案, 檔案內容如下
# comment
name1:password1
name2:password2:comment
name3:password3
注意, 密碼需要使用 openssl 產生,
例如, 我的密碼是 123
❯ openssl passwd 123
8uxCGNPhjFqiw
然後 htpasswd 檔案, 填入
# comment
user1:8uxCGNPhjFqiw:123
重新啟動 nginx, 就會發現要輸入帳密才能觀看,
修改 hosts
設定檔
Windows
hosts
路徑在
C:\WINDOWS\system32\drivers\etc\hosts
儲存時可能會要求權限。
MAC
hosts
路徑在
sudo vi /etc/hosts
查詢內網 ip
Windows
ipconfig
MAC
ifconfig
假如你看到的是 192.168.1.103 好了,這樣只要在相同的網路下(內網),你就可以
透過這 ip 直接連到你的網站 。
還有剛剛教大家修改 hosts
設定檔的部分,
我們可以直接瀏覽 http://twtrubiks.com/api/musics/
是一種管理 process 的工具,透過它可以很方便的啟動、停止、重啟以及監控一個
或多個 process。假如某個 process 掛了,當 Supervisor 發現時,會自動將他再重新
啟動,不需要寫任何程式 ( 不需要再自己寫 shell 來控制 )
這時候你一定又會問我? 這樣我要用它嗎 😕
哪時候需要使用 supervisor ?
當需要在同一個 container 中啟動多個獨立的 processes 時,你就適合使用 supervisor,
舉個例子,假如你在同一個 container 中 ,像是 Nginx + uWSGI + Django 都在同一個
container 時,你就適合使用 supervisor。
不過,如果是採用 Docker,一般會建議將 Nginx 和 uWSGI + Django 獨立出來比較好
( 也就是分成兩個 container),然後用 docker-compose 的方法管理多個 container,
也就是本範例的作法。
那你會問 ? 那我要如何管理 container 意外終止退出呢 😕
這時候可以參考 docker-compose.yml ,利用 restart=always
解決,他會在意外終止
時幫你重新啟動
如果不了解 CORS,請先閱讀 了解 Same-Origin Policy 以及 CORS 📝。
設定是 django + nginx + uwsgi,這時候我們先來思考一個問題,
這樣你可能會問我,CORS 我是要設定在 Nginx 上,還是 Django,又或是兩邊都設定呢 ❓
秉持實驗的精神,三種況狀都來嘗試 ( 建議看影片 ):
方法一 : 兩邊都設定 ( 失敗 )
如果你兩邊都設定,會出現類似以下的錯誤訊息
Access to XMLHttpRequest at 'http://127.0.0.1/api/musics/' from origin 'http://127.0.0.1:8000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The 'Access-Control-Allow-Origin' header contains multiple values '*, *', but only one is allowed.
方法二 : 只設定在 Nginx 上 ( 失敗 )
nginx 上設定 CORS,方法可參考 here,
補充說明,
Access-Control-Allow-Origin
允許的 domain,詳細說明可參考 Access-Control-Allow-Origin。
Access-Control-Allow-Credentials
詳細說明可參考 Access-Control-Allow-Credentials。
Access-Control-Allow-Methods
詳細說明可參考 Access-Control-Allow-Methods。
Access-Control-Allow-Headers
preflight request 指的是 CORS 發出的 OPTIONS request,
( 如果不知道什麼是 preflight request,可參考 預檢請求 )
actual request 指的是實際發出的 request,這邊是指 actual request。
詳細說明可參考 Access-Control-Allow-Headers。
Access-Control-Expose-Headers
指 broswer 可以使用/讀取那些 response 中的 headers,預設有這些 headers,
Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma
,
如果想拿到其他的 headers,就必須再手動加進去。
詳細說明可參考 Access-Control-Expose-Headers。
Access-Control-Max-Age
preflight request 可以被 Cache 多長的時間。在時間內,broswer 會使用 Cache。
詳細說明可參考 Access-Control-Max-Age。
如果你設定在 Nginx 上,你會發現你的 CORS headers 被吃掉了,
Access to XMLHttpRequest at 'http://127.0.0.1/api/musics/' from origin 'http://127.0.0.1:8000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
這個問題我暫時也找不到方法,相關 issuse。
( 如果有人找到方法,請和小弟說一下,我去嘗試看看 )
方法三 : 只設定在 Django 上 ( 成功 )
django 上可以設定 CORS,透過 django-cors-headers 方法可參考 文章。
所以,如果你的環境是 django + nginx + uwsgi,CORS 建議使用 django-cors-headers 設定在 Django 上。
使用的可參考 nginx.conf,
http {
......
log_format main '$host $remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'$request_time';
......
這邊我也放了範例的 log 給各位, 可參考 nginx-access.example_log
localhost 172.30.0.1 - - [05/Apr/2022:03:45:05 +0000] "GET /api/ HTTP/1.1" 200 1722 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:98.0) Gecko/20100101 Firefox/98.0" 0.106
localhost 172.30.0.1 - - [05/Apr/2022:03:46:05 +0000] "GET /api/ HTTP/1.1" 200 1722 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:98.0) Gecko/20100101 Firefox/98.0" 0.206
localhost 172.30.0.2 - - [06/Apr/2022:04:45:05 +0000] "GET /api/ HTTP/1.1" 200 1722 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:98.0) Gecko/20100101 Firefox/98.0" 0.306
localhost 172.30.0.2 - - [06/Apr/2022:04:47:05 +0000] "GET /api/ HTTP/1.1" 200 1722 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:98.0) Gecko/20100101 Firefox/98.0" 0.506
Page View 的縮寫, 可以簡單看成是一個 request 就是一個 PV.
計算所有的 PV 數
cat nginx-access.example_log | wc -l
計算某天的 PV 數
cat nginx-access.example_log | sed -n '/05\/Apr\/2022/p' | wc -l
計算某一個時間的 PV 數 (4-5)
cat nginx-access.example_log | sed -n '/06\/Apr\/2022:04/,/06\/Apr\/2022:05/p' | wc -l
計算每秒的 PV 數
awk '{print $5}' nginx-access.example_log | cut -c 11-18 | sort | uniq -c | sort -n -r| head -n 10
計算每分鐘的 PV 數
awk '{print $5}' nginx-access.example_log | cut -c 11-15 | sort | uniq -c | sort -n -r| head -n 10
計算每小時的 PV 數
awk '{print $5}' nginx-access.example_log | cut -c 11-12 | sort | uniq -c | sort -n -r| head -n 10
sort
必需執行主要是因為 uniq
的關係, 可參考 uniq.
Unique Visitor 的縮寫, 獨立的訪客, 每個訪客一天只算一次.
這邊簡單用 IP 來當作獨立的訪客,
依據 IP 計算 UV 數量
❯ awk '{print $2}' nginx-access.example_log | sort | uniq -c | wc -l
2
計算每個 IP 出現次數
❯ awk '{print $2}' nginx-access.example_log | sort | uniq -c | sort -n
2 172.30.0.1
2 172.30.0.2
計算訪問最頻繁的前 10 個 IP
awk '{print $2}' nginx-access.example_log | sort -n | uniq -c | sort -n -r | head -n 10
查詢某 IP 的訪問 URL 狀態
grep '172.30.0.2' nginx-access.example_log | awk '{print $8}' | sort | uniq -c | sort -n -r
查詢訪問最頻繁的 URL
awk '{print $8}' nginx-access.example_log | sort | uniq -c | sort -n
查詢訪問最頻繁的 URL ( 排除特定 URL )
grep -v "/api/" nginx-access.example_log | awk '{print $8}' | sort | uniq -c | sort -n -r
查詢傳輸時間超過 0.3 秒的頁面 ( 記得要在 nginx.conf 加入 $request_time
)
❯ cat nginx-access.example_log | awk '($NF > 0.3){print $21}' | sort -n | uniq -c | sort -n -r
1 0.506
1 0.306
查詢訪問最頻訪的 host
❯ awk '{print $1}' nginx-access.example_log | sort | uniq -c | sort -n
4 localhost
如果以上指令不熟, 可參考 紀錄一些 linux 的指令.
有些蜘蛛爬蟲真的品質很差, 甚至影響系統的速度, 這邊教大家如何阻擋,
到你的 nginx 設定中加入 (這邊阻擋 MJ12bot)
if ($http_user_agent ~* (MJ12bot) ) {
return 444;
}
~*
代表不區分大小寫
~
代表區分大小寫
如果想要 ban 很多爬蟲,
if ($http_user_agent ~* (MJ12bot|Semrush|DataForSeo|Yandex|Ahrefs|Petal|Dot)){
return 444;
}
( http response code 也有人使用 410, 但似乎更多人使用 444)
重啟 nginx 後, 使用以下指令檢查是否成功
❯ curl -I -A 'mj12bot' YOUR_DOMAIN
curl: (52) Empty reply from server
其他範例
curl -A "Mozilla/5.0 (compatible; SemrushBot/6~bl; +http://www.semrush.com/bot.html)" YOUR_DOMAIN
curl -A "Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://ahrefs.com/robot/)" YOUR_DOMAIN
curl -A "Mozilla/5.0 (Linux; Android 7.0;) AppleWebKit/537.36 (KHTML, like Gecko) Mobile Safari/537.36 (compatible; PetalBot;+https://webmaster.petalsearch.com/site/petalbot)" YOUR_DOMAIN
-I
, --head
Show document info only.
-A
, --user-agent <name>
Send User-Agent to server.
如果成功阻擋, 會和上面顯示一樣.
如果沒有成功阻擋, 會顯示正常的 200.
也可以參考 here
自己也是第一次建立 Django + Nginx + uWSGI + Postgres ,中間也搞了超久 😱,但我真心推薦 Docker,
用 Docker 玩這個真的很爽,玩壞了,就直接刪掉重來就好,神速快,而且透過這個練習,大家應該還會
看到 Nginx 其實有非常多的功能可以去把玩,像是 Load balance 之類的,可以更瞭解 Server,我也是透過
他才瞭解自己不懂的地方還是很多,總之,建議大家動手下去依照我的步驟玩玩看,相信多多少少會有收穫
,我也是 Docker 新手,如果我有任何講錯的地方,請麻煩大家和我說,我會再修改 😊
如果意猶未盡,延伸閱讀 😆
- Linux
- Mac
- Python 3.8.2
- windows 10
文章都是我自己研究內化後原創,如果有幫助到您,也想鼓勵我的話,歡迎請我喝一杯咖啡:laughing:
MIT license