├── .gitignore ├── LICENSE ├── README.md ├── api ├── Dockerfile ├── django_rest_framework_tutorial │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── manage.py ├── musics │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ └── views.py ├── requirements.txt ├── uwsgi.ini └── uwsgi_params ├── api2 ├── Dockerfile ├── django_rest_framework_tutorial │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── manage.py ├── musics │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ └── views.py ├── requirements.txt ├── uwsgi2.ini └── uwsgi_params ├── docker-compose.yml └── nginx ├── Dockerfile ├── my_nginx.conf ├── nginx.conf └── nginx_origin.conf /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-django-nginx-uwsgi-postgres-load-balance-tutorial 2 | 3 | 實戰 Docker + Django + Nginx + uWSGI + Postgres - **Load Balance** 📝 4 | 5 | 這篇文章主要接續上一篇 6 | 7 | [Docker + Django + Nginx + uWSGI + Postgres 基本教學 - 從無到有 ( Docker + Django + Nginx + uWSGI + Postgres Tutorial )](https://github.com/twtrubiks/docker-django-nginx-uswgi-postgres-tutorial) 8 | 9 | 繼續介紹,這次的重點會放在 Nginx 的 **Load Balance** 上 :blush: 10 | 11 | * [Youtube Tutorial PART 1 - 正向代理器 VS 反向代理器 - 簡介](https://youtu.be/R2I8BBXnJJ8) 12 | * [Youtube Tutorial PART 2 - Docker + Django + Nginx + Load Balance - 概念](https://youtu.be/W4EMOO-THGs) 13 | * [Youtube Tutorial PART 3 - Docker + Django + Nginx + Load Balance - 實戰](https://youtu.be/ChK8MtQUDf0) 14 | * [Youtube Tutorial PART 4 - Django + Nginx + Load Balance - Docker scale](https://youtu.be/w83_lV5tORI) 15 | 16 | ## 簡介 17 | 18 | 在開始介紹 Nginx 的 Load Balance 之前,我會先帶大家了解一些名詞。 19 | 20 | 還記得在 [上一篇](https://github.com/twtrubiks/docker-django-nginx-uswgi-postgres-tutorial) 教學中,提到 Nginx 可以設定 反向代理器 嗎? 21 | 22 | 那時候沒有詳細解釋什麼是 反向代理器 ,這邊我將先帶大家了解 23 | 24 | **正向代理器** 以及 **反向代理器** 的差別 :notebook: 25 | 26 | ### 正向代理器 VS 反向代理器 27 | 28 | #### 正向代理器 29 | 30 | 正向代理器是一個位於使用者端以及目標 Server 之間的 **代理伺服器 ( Proxy Server )**, 31 | 32 | 透過代理伺服器收發 Request / Respond 資料, 33 | 34 | ![](https://i.imgur.com/zWIk8QW.png) 35 | 36 | 如上圖,X 無法直接和 Z 請求資料,但是 Y 可以和 Z 請求資料 ( 於是 X 透過 Y 向 Z 請求資料 ), 37 | 38 | Y 得到資料後,再將資料回傳給 X,這樣 X 就能順利得到 Z 的資料了 。 39 | 40 | 通常正向代理器必須額外做一些設定才可以使用。 41 | 42 | 正向代理器,Client 和 Proxy 可視為同一個 LAN ,對 Server 是隱藏的。 43 | 44 | ![](https://i.imgur.com/MJ0Pr2q.png) 45 | 46 | 溫馨小提醒 :heart: 47 | 48 | * LAN ( Local Area Network ),又稱 區域網路 49 | 50 | * WAN ( Wide Area Network ),又稱 廣域網路 51 | 52 | * WLAN ( Wireless LAN ),又稱 無線區域網路 53 | 54 | 正向代理器是代理使用者端( Client 端 ),替使用者端( Client 端 )收發 Request / Respond, 55 | 56 | 讓真正的使用者對伺服器端( Server 端 )隱藏。 57 | 58 | 可能你自己也有使用過( 或正在使用 :grin: )正向代理器 ,只是你不知道而以,像最常見的就是 **翻牆 ( VPN )** :smirk: 59 | 60 | 今天主角是阿鬼,阿鬼無法瀏覽 FB,但可以瀏覽 Server A,而阿鬼發現 Server A 可以訪問 FB, 61 | 62 | 於是阿鬼就透過 Server A 去瀏覽 FB,整個流程是,阿鬼透過 Server A 去瀏覽 FB,Server A 收到 63 | 64 | 來自阿鬼的 Request 時,去瀏覽 FB,FB 再把 Response 回傳給 Server A,Server A 再把收到的 65 | 66 | Response 回傳給阿鬼,這樣阿鬼就能順利得到 FB 的資料,這就是透過 Proxy ( 正向代理器 )。 67 | 68 | #### 反向代理器 69 | 70 | 反向代理器則相反過來,對於使用者端來說,反向代理器就好像是目標 Server, 71 | 72 | 使用者端也不需要做額外的設定, 73 | 74 | ![](https://i.imgur.com/kUBGgKz.png) 75 | 76 | 如上圖,雖然整個流程還是 X->Y->Z,但因為不用特別設定,所以使用者端會感覺好像直接是 X->Z, 77 | 78 | 反向代理器,Proxy 和 Server 屬於一個 LAN,對 Client 隱藏 79 | 80 | ![](https://i.imgur.com/j6wnvnt.png) 81 | 82 | 反向代理器是代理伺服器端( Server 端 ),替伺服器端( Server 端 )收發 Request / Respond, 83 | 84 | 讓真正的伺服器( Server 端 )對使用者隱藏。 85 | 86 | 本次的主角 **Load Balance** 就是反向代理器的一種應用 :open_mouth: 87 | 88 | ## 教學 89 | 90 | 解釋了那麼多,我們要開始實戰 Nginx 的 Load Balance,範例一樣是使用 [上一篇](https://github.com/twtrubiks/docker-django-nginx-uswgi-postgres-tutorial) 做修改, 91 | 92 | 因為要做 Load Balance,所以增加一台主機( service ),名稱命名為 [api2](https://github.com/twtrubiks/docker-django-nginx-uwsgi-postgres-load-balance-tutorial/tree/master/api2),基本上內容 93 | 94 | 都和 [api](https://github.com/twtrubiks/docker-django-nginx-uwsgi-postgres-load-balance-tutorial/tree/master/api) 差不多,只是修改了一些名稱( 方便區別而已 ),可參考 [docker-compose.yml](https://github.com/twtrubiks/docker-django-nginx-uwsgi-postgres-load-balance-tutorial/blob/master/docker-compose.yml)。 95 | 96 | 整體的 Load Balance 的概念可以參考下圖 97 | 98 | ![](https://i.imgur.com/RxUtPiz.png) 99 | 100 | 這樣有什麼好處呢 ? 101 | 102 | 最直接的好處就是,如果一台掛了,整個系統還是不會死, 103 | 104 | 因為其他的 Server 還是正常工作:relaxed: 105 | 106 | 再來就是我們的主角 [my_nginx.conf](https://github.com/twtrubiks/docker-django-nginx-uwsgi-postgres-load-balance-tutorial/blob/master/nginx/my_nginx.conf),其實主要也是修改這邊的設定而已 :laughing: 107 | 108 | ```config 109 | # the upstream component nginx needs to connect to 110 | upstream uwsgi { 111 | # server api:8001; # use TCP 112 | server unix:/docker_api/app.sock weight=4 ; # for a file socket 113 | server unix:/docker_api2/app2.sock weight=6; # for a file socket 114 | } 115 | 116 | # configuration of the server 117 | server { 118 | # the port your site will be served on 119 | listen 80; 120 | # index index.html; 121 | # the domain name it will serve for 122 | # substitute your machine's IP address or FQDN 123 | server_name twtrubiks.com www.twtrubiks.com; 124 | charset utf-8; 125 | 126 | client_max_body_size 75M; # adjust to taste 127 | 128 | # Django media 129 | # location /media { 130 | # alias /docker_api/static/media; # your Django project's media files - amend as required 131 | # } 132 | 133 | location /static { 134 | alias /docker_api/static; # your Django project's static files - amend as required 135 | } 136 | 137 | location / { 138 | uwsgi_pass uwsgi; 139 | include /etc/nginx/uwsgi_params; # the uwsgi_params file you installed 140 | } 141 | 142 | } 143 | ``` 144 | 145 | 其實設定並不難,只是在 `upstream` 裡面增加一台 Server 而已,後面的 `weight` 則代表權重, 146 | 147 | Nginx 預設會使用 round-robin 演算法來實作 Load balance。 148 | 149 | ### Load balancing methods 150 | 151 | Nginx 主要的 Load balance 有三種 152 | 153 | ****round-robin — requests to the application servers are distributed in a round-robin fashion**** 154 | 155 | 使用輪詢的方式,也可以加上 `weight` 權重,讓比較強的 Server 承受比較大的壓力( `weight` 設定高一點)。 156 | 157 | ****least-connected — next request is assigned to the server with the least number of active connections**** 158 | 159 | 此種方式,Nginx 會將請求給負載 Loading 較輕的 Server,避免負載(A Server) Loading 已經很大了, 160 | 161 | 還將請求交給 A Server 處理。 162 | 163 | ****ip-hash — a hash-function is used to determine what server should be selected for the next request (based on the client's IP address)**** 164 | 165 | 不管是 round-robin 還是 least-connected 的 Load balance ,每個 User 送出的請求都可能會被分到不同的 166 | 167 | Server,並不能保證同一個 User 每次的請求都一定會連到同一台 Server, 168 | 因為這個原因,於是有了 ip-hash, 169 | 170 | 透過這個方法,會使用 ip 的方式將同一個 User 的請求都導到同一台 171 | Server (除非這台 Server 掛了 :sob: ) 172 | 173 | 介紹完了 Nginx 主要的三種 Load balance 演算法,至於要使用哪一種,就看你自己的需求了 :grinning: 174 | 175 | 更多範例以及詳細的說明可參考官網 176 | 177 | ( 建議閱讀 ,雖然我有幫大家整理重點出來了 :grinning: ) 178 | 179 | [http://nginx.org/en/docs/http/load_balancing.html](http://nginx.org/en/docs/http/load_balancing.html) 180 | 181 | ## 執行步驟 182 | 183 | 直接執行 `docker-compose up` 見證奇蹟 184 | 185 | 細部的圖就不再貼一次了,請參考 [上一篇](https://github.com/twtrubiks/docker-django-nginx-uswgi-postgres-tutorial) 186 | 187 | 會發現我們多了一台 `api2` 188 | 189 | ![](https://i.imgur.com/KhROEky.png) 190 | 191 | 接著開啟另一個 terminal,進入 `api` ( Django + uWSGI ) 的容器, 192 | 193 | 指令可參考之前的 [docker-tutorial-指令介紹](https://github.com/twtrubiks/docker-tutorial#指令介紹), 194 | 195 | ```cmd 196 | docker exec -it bash 197 | ``` 198 | 199 | 執行 migrate 200 | 201 | ```cmd 202 | python manage.py makemigrations musics 203 | python manage.py migrate 204 | ``` 205 | 206 | ![](https://i.imgur.com/nQDwP7e.png) 207 | 208 | 建立 user 209 | 210 | ```cmd 211 | python manage.py createsuperuser 212 | ``` 213 | 214 | ![](https://i.imgur.com/saaDD7R.png) 215 | 216 | 最後再執行 217 | 218 | ```cmd 219 | python manage.py collectstatic 220 | ``` 221 | 222 | ![](https://i.imgur.com/1EKNNOx.png) 223 | 224 | 將 Django 中的 static files 收集起來,變成 static folder, 225 | 226 | 我們不需要再進入 `api2` 的 container 執行剛剛的動作,因為我們都是連同一個 db, 227 | 228 | 到這裡基本上就是完工了:smile: 229 | 230 | 瀏覽 [http://localhost:8080/api/music/](http://localhost:8080/api/music/) 確認是否正常。 231 | 232 | ## 執行畫面 233 | 234 | 瀏覽 [http://localhost:8080/api/music/](http://localhost:8080/api/music/) 235 | 236 | ![](https://i.imgur.com/jl43jST.png) 237 | 238 | ![](https://i.imgur.com/Fw6LjbE.png) 239 | 240 | 那要怎麼確定真的有 Load balance 呢? 241 | 242 | 可以在頁面上瘋狂點擊 F5,然後我們觀察 terminal,你會發現 243 | 244 | ![](https://i.imgur.com/7xuFXw5.png) 245 | 246 | 有時候是 `api`,有時候是 `api2`,還記得前面設定的 `weight` 權重, 247 | 248 | `api` 和 `api2` 是 4:6,4:6 是什麼意思呢:question::question: 249 | 250 | 假如現在有 10 個 Request 進來,4 個 Request 會交給 `api` 這個 Server 處理, 251 | 252 | 而剩下的 6 個 Request 則會交給 `api2` 這個 Server 處理 :+1: 253 | 254 | `weight` 權重可以依照自己的需求設定。 255 | 256 | 接著再來模擬一台 Server 掛了的時候,如下圖,Server 因為某種原因掛了, 257 | 258 | ![](https://i.imgur.com/fRa1Q9t.png) 259 | 260 | 先透過 stop 指令將 `api` container 停止 261 | 262 | ( 模擬 `api` 這台 Server 掛了 ) 263 | 264 | ```cmd 265 | docker stop [OPTIONS] CONTAINER [CONTAINER...] 266 | ``` 267 | 268 | ![](https://i.imgur.com/LkoQeDc.png) 269 | 270 | ![](https://i.imgur.com/lHmMPUu.png) 271 | 272 | 然後再繼續瀏覽 [http://localhost:8080/api/music/](http://localhost:8080/api/music/),你會發現網站正常 work ( 確實沒有掛掉 ), 273 | 274 | 而且 terminal 現在都只會輸出 `api2`, 275 | 276 | ![](https://i.imgur.com/RTdzQqX.png) 277 | 278 | 因為我們已經將 `api` 停止了(模擬機器意外掛了), 279 | 280 | 有沒有很酷 :satisfied: 這就是最簡單的 Load balance :flushed: 281 | 282 | 範例是總共有兩台 server ( `api` 以及 `api2` ), 你也可以自己多新增幾台來玩玩看。 283 | 284 | ## 其他的負載平衡 285 | 286 | 除了 Nginx 的 Load banlance 之外,假設你的系統更龐大且有規模, 287 | 288 | 可以考慮使用更專業的負載平衡,像是 [HAProxy](http://www.haproxy.org/) ( High Availability Proxy ) , 289 | 290 | 如果要專注在 Load banlance 上,選擇 [HAProxy](http://www.haproxy.org/) 應該會比 Nginx 的 291 | 292 | Load banlance 還來的好。 293 | 294 | 如果對 HAProxy 有興趣,可參考 [Docker Swarm + HAProxy](https://github.com/twtrubiks/docker-swarm-tutorial#docker-swarm--haproxy) :sunglasses: 295 | 296 | ## 後記: 297 | 298 | 這次主要介紹了 Nginx 的 Load Banlance 給大家,建議大家可以動手玩玩看,整體會比較有感覺 :smiley: 299 | 300 | 這次嘗試參考網路上的圖自己簡單的畫一遍,希望對大家多多少少有幫助:flushed: 301 | 302 | 如果有在玩像是 AWS 的人,可以知道還有一種東西更狂,就是 異地同步備份,大家有興趣自行研究:sunglasses: 303 | 304 | 如果有任何講錯的地方,請麻煩大家和我說,我會再修改,感謝各位的閱讀:v: 305 | 306 | 下一步可以試試看用 Docker scale 的方法來完成( 更好的寫法 ),可參考 [better](https://github.com/twtrubiks/docker-django-nginx-uwsgi-postgres-load-balance-tutorial/tree/better) 分支。 307 | 308 | 如果意猶未盡,延伸閱讀 :satisfied: 309 | 310 | * [Docker Swarm 基本教學 - 從無到有 Docker-Swarm-Beginners-Guide📝](https://github.com/twtrubiks/docker-swarm-tutorial) 311 | 312 | ## 執行環境 313 | 314 | * Mac 315 | * Python 3.6.2 316 | * windows 10 317 | 318 | ## Reference 319 | 320 | * [https://docs.docker.com/](https://docs.docker.com/) 321 | * [nginx-load_balancing](http://nginx.org/en/docs/http/load_balancing.html) 322 | 323 | ## Donation 324 | 325 | 文章都是我自己研究內化後原創,如果有幫助到您,也想鼓勵我的話,歡迎請我喝一杯咖啡:laughing: 326 | 327 | ![alt tag](https://i.imgur.com/LRct9xa.png) 328 | 329 | [贊助者付款](https://payment.opay.tw/Broadcaster/Donate/9E47FDEF85ABE383A0F5FC6A218606F8) 330 | 331 | ## License 332 | 333 | MIT license 334 | -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6.2 2 | LABEL maintainer twtrubiks 3 | ENV PYTHONUNBUFFERED 1 4 | RUN mkdir /docker_api 5 | WORKDIR /docker_api 6 | COPY . /docker_api 7 | RUN pip install -r requirements.txt 8 | # RUN pip install -i https://pypi.python.org/simple/ -r requirements.txt -------------------------------------------------------------------------------- /api/django_rest_framework_tutorial/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twtrubiks/docker-django-nginx-uwsgi-postgres-load-balance-tutorial/27a98a5c472feef40fc44e50096cfe8c456be928/api/django_rest_framework_tutorial/__init__.py -------------------------------------------------------------------------------- /api/django_rest_framework_tutorial/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django_rest_framework_tutorial project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.10.6. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.10/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'g9je0wxn58$wico&t@@k6@1$yu)gv$cch7yz*9bin4&8$m@ulb' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = ['*'] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'rest_framework', 41 | 'musics', 42 | ] 43 | 44 | MIDDLEWARE = [ 45 | 'django.middleware.security.SecurityMiddleware', 46 | 'django.contrib.sessions.middleware.SessionMiddleware', 47 | 'django.middleware.common.CommonMiddleware', 48 | 'django.middleware.csrf.CsrfViewMiddleware', 49 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | ] 53 | 54 | ROOT_URLCONF = 'django_rest_framework_tutorial.urls' 55 | 56 | TEMPLATES = [ 57 | { 58 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 59 | 'DIRS': [os.path.join(BASE_DIR, 'templates')] 60 | , 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.request', 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = 'django_rest_framework_tutorial.wsgi.application' 74 | 75 | 76 | # Database 77 | # https://docs.djangoproject.com/en/1.10/ref/settings/#databases 78 | 79 | # DATABASES = { 80 | # 'default': { 81 | # 'ENGINE': 'django.db.backends.sqlite3', 82 | # 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 83 | # } 84 | # } 85 | 86 | DATABASES = { 87 | 'default': { 88 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 89 | 'NAME': 'postgres', 90 | 'USER': 'postgres', 91 | 'PASSWORD': 'password123', 92 | 'HOST': 'db', 93 | 'PORT': 5432, 94 | } 95 | } 96 | 97 | 98 | # Password validation 99 | # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators 100 | 101 | AUTH_PASSWORD_VALIDATORS = [ 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 104 | }, 105 | { 106 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 107 | }, 108 | { 109 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 110 | }, 111 | { 112 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 113 | }, 114 | ] 115 | 116 | 117 | # Internationalization 118 | # https://docs.djangoproject.com/en/1.10/topics/i18n/ 119 | 120 | LANGUAGE_CODE = 'en-us' 121 | 122 | TIME_ZONE = 'UTC' 123 | 124 | USE_I18N = True 125 | 126 | USE_L10N = True 127 | 128 | USE_TZ = True 129 | 130 | 131 | # Static files (CSS, JavaScript, Images) 132 | # https://docs.djangoproject.com/en/1.10/howto/static-files/ 133 | 134 | STATIC_URL = '/static/' 135 | STATIC_ROOT = os.path.join(BASE_DIR, 'static/') 136 | # MEDIA_ROOT = os.path.join(BASE_DIR, 'media/') -------------------------------------------------------------------------------- /api/django_rest_framework_tutorial/urls.py: -------------------------------------------------------------------------------- 1 | """django_rest_framework_tutorial URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.10/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url, include 17 | from django.contrib import admin 18 | from rest_framework.routers import DefaultRouter 19 | from musics import views 20 | 21 | router = DefaultRouter() 22 | router.register(r'music', views.MusicViewSet, base_name='music') 23 | 24 | urlpatterns = [ 25 | url(r'^admin/', admin.site.urls), 26 | url(r'^api/', include(router.urls, namespace='api'), name='api'), 27 | url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) 28 | ] 29 | -------------------------------------------------------------------------------- /api/django_rest_framework_tutorial/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_rest_framework_tutorial project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_rest_framework_tutorial.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /api/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_rest_framework_tutorial.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /api/musics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twtrubiks/docker-django-nginx-uwsgi-postgres-load-balance-tutorial/27a98a5c472feef40fc44e50096cfe8c456be928/api/musics/__init__.py -------------------------------------------------------------------------------- /api/musics/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /api/musics/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MusicsConfig(AppConfig): 5 | name = 'musics' 6 | -------------------------------------------------------------------------------- /api/musics/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | # Create your models here. 5 | class Music(models.Model): 6 | song = models.TextField() 7 | singer = models.TextField() 8 | last_modify_date = models.DateTimeField(auto_now=True) 9 | created = models.DateTimeField(auto_now_add=True) 10 | 11 | class Meta: 12 | db_table = "music" 13 | -------------------------------------------------------------------------------- /api/musics/serializers.py: -------------------------------------------------------------------------------- 1 | from django.utils.timezone import now 2 | from rest_framework import serializers 3 | from musics.models import Music 4 | 5 | 6 | class MusicSerializer(serializers.ModelSerializer): 7 | days_since_created = serializers.SerializerMethodField() 8 | 9 | class Meta: 10 | model = Music 11 | # fields = '__all__' 12 | fields = ('id', 'song', 'singer', 'last_modify_date', 'created', 'days_since_created') 13 | 14 | def get_days_since_created(self, obj): 15 | return (now() - obj.created).days 16 | -------------------------------------------------------------------------------- /api/musics/tests.py: -------------------------------------------------------------------------------- 1 | # Create your tests here. 2 | from django.contrib.auth.models import User 3 | from rest_framework import status 4 | from rest_framework.reverse import reverse 5 | from rest_framework.test import APITestCase, APIClient 6 | 7 | from musics.models import Music 8 | 9 | 10 | class MusicViewTestCase(APITestCase): 11 | url_reverse = reverse('api:music-list') 12 | url = '/api/music/' 13 | url_detail = '/api/music/{}/' 14 | url_detail_route_reverse = reverse('api:music-detail', kwargs={"pk": 1}) 15 | url_detail_route = '/api/music/{}/detail/' 16 | url_list_route = '/api/music/all_singer/' 17 | 18 | def setUp(self): 19 | print('setUp') 20 | 21 | self.client = APIClient() 22 | # create user 23 | User.objects.create_user(username='test_user', password='password123') 24 | 25 | self.client.login(username='test_user', password='password123') 26 | 27 | self.request_data = { 28 | 'song': 'song_test', 29 | 'singer': 'singer_test' 30 | } 31 | 32 | self.music = Music.objects.create(song='song_test', singer='singer_test') 33 | 34 | def test_api_music_create(self): 35 | print('test_api_music_create') 36 | self.response = self.client.post( 37 | self.url, 38 | self.request_data, 39 | format="json" 40 | ) 41 | self.assertEqual(self.response.status_code, status.HTTP_201_CREATED) 42 | self.assertEqual(Music.objects.count(), 2) 43 | self.assertEqual(Music.objects.get(pk=self.music.id).song, 'song_test') 44 | self.assertEqual(Music.objects.get(pk=self.music.id).singer, 'singer_test') 45 | 46 | def test_api_music_retrieve(self): 47 | print('test_api_music_retrieve') 48 | music = Music.objects.get(pk=self.music.id) 49 | response = self.client.get(self.url_detail.format(self.music.id)) 50 | self.assertEqual(response.status_code, status.HTTP_200_OK) 51 | self.assertEqual(response.data.get('song', None), music.song) 52 | self.assertEqual(response.data.get('singer', None), music.singer) 53 | 54 | def test_api_music_partial_update(self): 55 | print('test_api_music_partial_update') 56 | update_song = {'song': 'song_update'} 57 | response = self.client.patch(self.url_detail.format(self.music.id), update_song, format='json') 58 | self.assertEqual(response.status_code, status.HTTP_200_OK) 59 | self.assertEqual(response.data.get('song', None), update_song.get('song', None)) 60 | 61 | def test_api_music_update(self): 62 | print('test_api_music_update') 63 | update_song = {'song': 'song_update', 'singer': 'singer_update'} 64 | response = self.client.put(self.url_detail.format(self.music.id), update_song, format='json') 65 | self.assertEqual(response.status_code, status.HTTP_200_OK) 66 | self.assertEqual(response.data.get('song', None), update_song.get('song')) 67 | self.assertEqual(response.data.get('singer', None), update_song.get('singer')) 68 | 69 | def test_api_music_delete(self): 70 | print('test_api_music_delete') 71 | response = self.client.delete(self.url_detail.format(self.music.id)) 72 | self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) 73 | 74 | def test_api_music_detail_route(self): 75 | print('test_api_music_detail_route') 76 | music = Music.objects.get(pk=self.music.id) 77 | response = self.client.get(self.url_detail_route.format(self.music.id)) 78 | self.assertEqual(response.status_code, status.HTTP_200_OK) 79 | self.assertEqual(response.data.get('song', None), music.song) 80 | self.assertEqual(response.data.get('singer', None), music.singer) 81 | 82 | def test_api_music_list_route(self): 83 | print('test_api_music_list_route') 84 | music = Music.objects.values_list('singer', flat=True).distinct() 85 | response = self.client.get(self.url_list_route) 86 | self.assertEqual(response.status_code, status.HTTP_200_OK) 87 | self.assertEqual(next(iter(response.data)), next(iter(music))) 88 | -------------------------------------------------------------------------------- /api/musics/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | from django.shortcuts import get_object_or_404 3 | from musics.models import Music 4 | from musics.serializers import MusicSerializer 5 | 6 | from rest_framework import viewsets, status 7 | from rest_framework.permissions import IsAuthenticated 8 | from rest_framework.response import Response 9 | from rest_framework.parsers import JSONParser 10 | from rest_framework.decorators import detail_route, list_route 11 | 12 | 13 | # Create your views here. 14 | class MusicViewSet(viewsets.ModelViewSet): 15 | queryset = Music.objects.all() 16 | serializer_class = MusicSerializer 17 | permission_classes = (IsAuthenticated,) 18 | parser_classes = (JSONParser,) 19 | 20 | # /api/music/{pk}/detail/ 21 | @detail_route(methods=['get']) 22 | def detail(self, request, pk=None): 23 | music = get_object_or_404(Music, pk=pk) 24 | result = { 25 | 'singer': music.singer, 26 | 'song': music.song 27 | } 28 | 29 | return Response(result, status=status.HTTP_200_OK) 30 | 31 | # /api/music/all_singer/ 32 | @list_route(methods=['get']) 33 | def all_singer(self, request): 34 | music = Music.objects.values_list('singer', flat=True).distinct() 35 | return Response(music, status=status.HTTP_200_OK) 36 | -------------------------------------------------------------------------------- /api/requirements.txt: -------------------------------------------------------------------------------- 1 | django<2.0 2 | 3 | # https://www.django-rest-framework.org/community/3.8-announcement/#deprecations 4 | djangorestframework==3.8.1 5 | 6 | psycopg2 7 | uwsgi -------------------------------------------------------------------------------- /api/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | 3 | socket=app.sock 4 | master=true 5 | # maximum number of worker processes 6 | processes=4 7 | threads=2 8 | # Django's wsgi file 9 | module=django_rest_framework_tutorial.wsgi:application 10 | 11 | # chmod-socket=664 12 | # uid=www-data 13 | # gid=www-data 14 | 15 | # clear environment on exit 16 | vacuum = true -------------------------------------------------------------------------------- /api/uwsgi_params: -------------------------------------------------------------------------------- 1 | uwsgi_param QUERY_STRING $query_string; 2 | uwsgi_param REQUEST_METHOD $request_method; 3 | uwsgi_param CONTENT_TYPE $content_type; 4 | uwsgi_param CONTENT_LENGTH $content_length; 5 | 6 | uwsgi_param REQUEST_URI $request_uri; 7 | uwsgi_param PATH_INFO $document_uri; 8 | uwsgi_param DOCUMENT_ROOT $document_root; 9 | uwsgi_param SERVER_PROTOCOL $server_protocol; 10 | uwsgi_param REQUEST_SCHEME $scheme; 11 | uwsgi_param HTTPS $https if_not_empty; 12 | 13 | uwsgi_param REMOTE_ADDR $remote_addr; 14 | uwsgi_param REMOTE_PORT $remote_port; 15 | uwsgi_param SERVER_PORT $server_port; 16 | uwsgi_param SERVER_NAME $server_name; -------------------------------------------------------------------------------- /api2/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6.2 2 | LABEL maintainer twtrubiks 3 | ENV PYTHONUNBUFFERED 1 4 | RUN mkdir /docker_api2 5 | WORKDIR /docker_api2 6 | COPY . /docker_api2 7 | RUN pip install -r requirements.txt 8 | # RUN pip install -i https://pypi.python.org/simple/ -r requirements.txt -------------------------------------------------------------------------------- /api2/django_rest_framework_tutorial/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twtrubiks/docker-django-nginx-uwsgi-postgres-load-balance-tutorial/27a98a5c472feef40fc44e50096cfe8c456be928/api2/django_rest_framework_tutorial/__init__.py -------------------------------------------------------------------------------- /api2/django_rest_framework_tutorial/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django_rest_framework_tutorial project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.10.6. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.10/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'g9je0wxn58$wico&t@@k6@1$yu)gv$cch7yz*9bin4&8$m@ulb' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = ['*'] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'rest_framework', 41 | 'musics', 42 | ] 43 | 44 | MIDDLEWARE = [ 45 | 'django.middleware.security.SecurityMiddleware', 46 | 'django.contrib.sessions.middleware.SessionMiddleware', 47 | 'django.middleware.common.CommonMiddleware', 48 | 'django.middleware.csrf.CsrfViewMiddleware', 49 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | ] 53 | 54 | ROOT_URLCONF = 'django_rest_framework_tutorial.urls' 55 | 56 | TEMPLATES = [ 57 | { 58 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 59 | 'DIRS': [os.path.join(BASE_DIR, 'templates')] 60 | , 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.request', 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = 'django_rest_framework_tutorial.wsgi.application' 74 | 75 | 76 | # Database 77 | # https://docs.djangoproject.com/en/1.10/ref/settings/#databases 78 | 79 | # DATABASES = { 80 | # 'default': { 81 | # 'ENGINE': 'django.db.backends.sqlite3', 82 | # 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 83 | # } 84 | # } 85 | 86 | DATABASES = { 87 | 'default': { 88 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 89 | 'NAME': 'postgres', 90 | 'USER': 'postgres', 91 | 'PASSWORD': 'password123', 92 | 'HOST': 'db', 93 | 'PORT': 5432, 94 | } 95 | } 96 | 97 | 98 | # Password validation 99 | # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators 100 | 101 | AUTH_PASSWORD_VALIDATORS = [ 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 104 | }, 105 | { 106 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 107 | }, 108 | { 109 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 110 | }, 111 | { 112 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 113 | }, 114 | ] 115 | 116 | 117 | # Internationalization 118 | # https://docs.djangoproject.com/en/1.10/topics/i18n/ 119 | 120 | LANGUAGE_CODE = 'en-us' 121 | 122 | TIME_ZONE = 'UTC' 123 | 124 | USE_I18N = True 125 | 126 | USE_L10N = True 127 | 128 | USE_TZ = True 129 | 130 | 131 | # Static files (CSS, JavaScript, Images) 132 | # https://docs.djangoproject.com/en/1.10/howto/static-files/ 133 | 134 | STATIC_URL = '/static/' 135 | STATIC_ROOT = os.path.join(BASE_DIR, 'static/') 136 | # MEDIA_ROOT = os.path.join(BASE_DIR, 'media/') -------------------------------------------------------------------------------- /api2/django_rest_framework_tutorial/urls.py: -------------------------------------------------------------------------------- 1 | """django_rest_framework_tutorial URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.10/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url, include 17 | from django.contrib import admin 18 | from rest_framework.routers import DefaultRouter 19 | from musics import views 20 | 21 | router = DefaultRouter() 22 | router.register(r'music', views.MusicViewSet, base_name='music') 23 | 24 | urlpatterns = [ 25 | url(r'^admin/', admin.site.urls), 26 | url(r'^api/', include(router.urls, namespace='api'), name='api'), 27 | url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) 28 | ] 29 | -------------------------------------------------------------------------------- /api2/django_rest_framework_tutorial/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_rest_framework_tutorial project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_rest_framework_tutorial.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /api2/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_rest_framework_tutorial.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /api2/musics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twtrubiks/docker-django-nginx-uwsgi-postgres-load-balance-tutorial/27a98a5c472feef40fc44e50096cfe8c456be928/api2/musics/__init__.py -------------------------------------------------------------------------------- /api2/musics/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /api2/musics/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MusicsConfig(AppConfig): 5 | name = 'musics' 6 | -------------------------------------------------------------------------------- /api2/musics/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | # Create your models here. 5 | class Music(models.Model): 6 | song = models.TextField() 7 | singer = models.TextField() 8 | last_modify_date = models.DateTimeField(auto_now=True) 9 | created = models.DateTimeField(auto_now_add=True) 10 | 11 | class Meta: 12 | db_table = "music" 13 | -------------------------------------------------------------------------------- /api2/musics/serializers.py: -------------------------------------------------------------------------------- 1 | from django.utils.timezone import now 2 | from rest_framework import serializers 3 | from musics.models import Music 4 | 5 | 6 | class MusicSerializer(serializers.ModelSerializer): 7 | days_since_created = serializers.SerializerMethodField() 8 | 9 | class Meta: 10 | model = Music 11 | # fields = '__all__' 12 | fields = ('id', 'song', 'singer', 'last_modify_date', 'created', 'days_since_created') 13 | 14 | def get_days_since_created(self, obj): 15 | return (now() - obj.created).days 16 | -------------------------------------------------------------------------------- /api2/musics/tests.py: -------------------------------------------------------------------------------- 1 | # Create your tests here. 2 | from django.contrib.auth.models import User 3 | from rest_framework import status 4 | from rest_framework.reverse import reverse 5 | from rest_framework.test import APITestCase, APIClient 6 | 7 | from musics.models import Music 8 | 9 | 10 | class MusicViewTestCase(APITestCase): 11 | url_reverse = reverse('api:music-list') 12 | url = '/api/music/' 13 | url_detail = '/api/music/{}/' 14 | url_detail_route_reverse = reverse('api:music-detail', kwargs={"pk": 1}) 15 | url_detail_route = '/api/music/{}/detail/' 16 | url_list_route = '/api/music/all_singer/' 17 | 18 | def setUp(self): 19 | print('setUp') 20 | 21 | self.client = APIClient() 22 | # create user 23 | User.objects.create_user(username='test_user', password='password123') 24 | 25 | self.client.login(username='test_user', password='password123') 26 | 27 | self.request_data = { 28 | 'song': 'song_test', 29 | 'singer': 'singer_test' 30 | } 31 | 32 | self.music = Music.objects.create(song='song_test', singer='singer_test') 33 | 34 | def test_api_music_create(self): 35 | print('test_api_music_create') 36 | self.response = self.client.post( 37 | self.url, 38 | self.request_data, 39 | format="json" 40 | ) 41 | self.assertEqual(self.response.status_code, status.HTTP_201_CREATED) 42 | self.assertEqual(Music.objects.count(), 2) 43 | self.assertEqual(Music.objects.get(pk=self.music.id).song, 'song_test') 44 | self.assertEqual(Music.objects.get(pk=self.music.id).singer, 'singer_test') 45 | 46 | def test_api_music_retrieve(self): 47 | print('test_api_music_retrieve') 48 | music = Music.objects.get(pk=self.music.id) 49 | response = self.client.get(self.url_detail.format(self.music.id)) 50 | self.assertEqual(response.status_code, status.HTTP_200_OK) 51 | self.assertEqual(response.data.get('song', None), music.song) 52 | self.assertEqual(response.data.get('singer', None), music.singer) 53 | 54 | def test_api_music_partial_update(self): 55 | print('test_api_music_partial_update') 56 | update_song = {'song': 'song_update'} 57 | response = self.client.patch(self.url_detail.format(self.music.id), update_song, format='json') 58 | self.assertEqual(response.status_code, status.HTTP_200_OK) 59 | self.assertEqual(response.data.get('song', None), update_song.get('song', None)) 60 | 61 | def test_api_music_update(self): 62 | print('test_api_music_update') 63 | update_song = {'song': 'song_update', 'singer': 'singer_update'} 64 | response = self.client.put(self.url_detail.format(self.music.id), update_song, format='json') 65 | self.assertEqual(response.status_code, status.HTTP_200_OK) 66 | self.assertEqual(response.data.get('song', None), update_song.get('song')) 67 | self.assertEqual(response.data.get('singer', None), update_song.get('singer')) 68 | 69 | def test_api_music_delete(self): 70 | print('test_api_music_delete') 71 | response = self.client.delete(self.url_detail.format(self.music.id)) 72 | self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) 73 | 74 | def test_api_music_detail_route(self): 75 | print('test_api_music_detail_route') 76 | music = Music.objects.get(pk=self.music.id) 77 | response = self.client.get(self.url_detail_route.format(self.music.id)) 78 | self.assertEqual(response.status_code, status.HTTP_200_OK) 79 | self.assertEqual(response.data.get('song', None), music.song) 80 | self.assertEqual(response.data.get('singer', None), music.singer) 81 | 82 | def test_api_music_list_route(self): 83 | print('test_api_music_list_route') 84 | music = Music.objects.values_list('singer', flat=True).distinct() 85 | response = self.client.get(self.url_list_route) 86 | self.assertEqual(response.status_code, status.HTTP_200_OK) 87 | self.assertEqual(next(iter(response.data)), next(iter(music))) 88 | -------------------------------------------------------------------------------- /api2/musics/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | from django.shortcuts import get_object_or_404 3 | from musics.models import Music 4 | from musics.serializers import MusicSerializer 5 | 6 | from rest_framework import viewsets, status 7 | from rest_framework.permissions import IsAuthenticated 8 | from rest_framework.response import Response 9 | from rest_framework.parsers import JSONParser 10 | from rest_framework.decorators import detail_route, list_route 11 | 12 | 13 | # Create your views here. 14 | class MusicViewSet(viewsets.ModelViewSet): 15 | queryset = Music.objects.all() 16 | serializer_class = MusicSerializer 17 | permission_classes = (IsAuthenticated,) 18 | parser_classes = (JSONParser,) 19 | 20 | # /api/music/{pk}/detail/ 21 | @detail_route(methods=['get']) 22 | def detail(self, request, pk=None): 23 | music = get_object_or_404(Music, pk=pk) 24 | result = { 25 | 'singer': music.singer, 26 | 'song': music.song 27 | } 28 | 29 | return Response(result, status=status.HTTP_200_OK) 30 | 31 | # /api/music/all_singer/ 32 | @list_route(methods=['get']) 33 | def all_singer(self, request): 34 | music = Music.objects.values_list('singer', flat=True).distinct() 35 | return Response(music, status=status.HTTP_200_OK) 36 | -------------------------------------------------------------------------------- /api2/requirements.txt: -------------------------------------------------------------------------------- 1 | django 2 | djangorestframework 3 | psycopg2 4 | uwsgi -------------------------------------------------------------------------------- /api2/uwsgi2.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | 3 | socket=app2.sock 4 | master=true 5 | # maximum number of worker processes 6 | processes=4 7 | threads=2 8 | # Django's wsgi file 9 | module=django_rest_framework_tutorial.wsgi:application 10 | 11 | # chmod-socket=664 12 | # uid=www-data 13 | # gid=www-data 14 | 15 | # clear environment on exit 16 | vacuum = true -------------------------------------------------------------------------------- /api2/uwsgi_params: -------------------------------------------------------------------------------- 1 | uwsgi_param QUERY_STRING $query_string; 2 | uwsgi_param REQUEST_METHOD $request_method; 3 | uwsgi_param CONTENT_TYPE $content_type; 4 | uwsgi_param CONTENT_LENGTH $content_length; 5 | 6 | uwsgi_param REQUEST_URI $request_uri; 7 | uwsgi_param PATH_INFO $document_uri; 8 | uwsgi_param DOCUMENT_ROOT $document_root; 9 | uwsgi_param SERVER_PROTOCOL $server_protocol; 10 | uwsgi_param REQUEST_SCHEME $scheme; 11 | uwsgi_param HTTPS $https if_not_empty; 12 | 13 | uwsgi_param REMOTE_ADDR $remote_addr; 14 | uwsgi_param REMOTE_PORT $remote_port; 15 | uwsgi_param SERVER_PORT $server_port; 16 | uwsgi_param SERVER_NAME $server_name; -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | 4 | db: 5 | container_name: postgres 6 | image: postgres 7 | environment: 8 | POSTGRES_PASSWORD: password123 9 | ports: 10 | - "5432:5432" 11 | # (HOST:CONTAINER) 12 | volumes: 13 | - pgdata:/var/lib/postgresql/data/ 14 | 15 | nginx: 16 | container_name: nginx-container 17 | build: ./nginx 18 | restart: always 19 | ports: 20 | - "8080:80" 21 | volumes: 22 | - api_data:/docker_api 23 | - api_data2:/docker_api2 24 | - ./log:/var/log/nginx 25 | depends_on: 26 | - api 27 | - api2 28 | 29 | api: 30 | container_name: api_1 31 | build: ./api 32 | restart: always 33 | # command: uwsgi --emperor uwsgi.ini 34 | command: uwsgi --ini uwsgi.ini 35 | ports: 36 | - "8002:8000" 37 | volumes: 38 | - api_data:/docker_api 39 | depends_on: 40 | - db 41 | 42 | api2: 43 | container_name: api2_1 44 | build: ./api2 45 | restart: always 46 | # command: uwsgi --emperor uwsgi.ini 47 | command: uwsgi --ini uwsgi2.ini 48 | ports: 49 | - "8003:8001" 50 | volumes: 51 | - api_data2:/docker_api2 52 | depends_on: 53 | - db 54 | 55 | volumes: 56 | api_data: 57 | api_data2: 58 | pgdata: 59 | 60 | -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:latest 2 | 3 | COPY nginx.conf /etc/nginx/nginx.conf 4 | COPY my_nginx.conf /etc/nginx/sites-available/ 5 | 6 | RUN mkdir -p /etc/nginx/sites-enabled/\ 7 | && ln -s /etc/nginx/sites-available/my_nginx.conf /etc/nginx/sites-enabled/ 8 | 9 | # RUN mkdir -p /etc/nginx/sites-enabled/\ 10 | # && ln -s /etc/nginx/sites-available/my_nginx.conf /etc/nginx/sites-enabled/\ 11 | # && rm /etc/nginx/conf.d/default.conf 12 | 13 | CMD ["nginx", "-g", "daemon off;"] 14 | 15 | -------------------------------------------------------------------------------- /nginx/my_nginx.conf: -------------------------------------------------------------------------------- 1 | 2 | # the upstream component nginx needs to connect to 3 | upstream uwsgi { 4 | # server api:8001; # use TCP 5 | server unix:/docker_api/app.sock weight=4; # for a file socket 6 | server unix:/docker_api2/app2.sock weight=6; # for a file socket 7 | } 8 | 9 | # configuration of the server 10 | server { 11 | # the port your site will be served on 12 | listen 80; 13 | # index index.html; 14 | # the domain name it will serve for 15 | # substitute your machine's IP address or FQDN 16 | server_name twtrubiks.com www.twtrubiks.com; 17 | charset utf-8; 18 | 19 | client_max_body_size 75M; # adjust to taste 20 | 21 | # Django media 22 | # location /media { 23 | # alias /docker_api/static/media; # your Django project's media files - amend as required 24 | # } 25 | 26 | location /static { 27 | alias /docker_api/static; # your Django project's static files - amend as required 28 | } 29 | 30 | location / { 31 | uwsgi_pass uwsgi; 32 | include /etc/nginx/uwsgi_params; # the uwsgi_params file you installed 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | user root; 2 | worker_processes 1; 3 | 4 | error_log /var/log/nginx/error.log warn; 5 | pid /var/run/nginx.pid; 6 | 7 | 8 | events { 9 | worker_connections 1024; 10 | } 11 | 12 | 13 | http { 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 17 | '$status $body_bytes_sent "$http_referer" ' 18 | '"$http_user_agent" "$http_x_forwarded_for"'; 19 | 20 | access_log /var/log/nginx/access.log main; 21 | 22 | sendfile on; 23 | #tcp_nopush on; 24 | 25 | keepalive_timeout 65; 26 | 27 | #gzip on; 28 | 29 | # include /etc/nginx/conf.d/*.conf; 30 | include /etc/nginx/sites-available/*; 31 | } -------------------------------------------------------------------------------- /nginx/nginx_origin.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes 1; 3 | 4 | error_log /var/log/nginx/error.log warn; 5 | pid /var/run/nginx.pid; 6 | 7 | 8 | events { 9 | worker_connections 1024; 10 | } 11 | 12 | 13 | http { 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | 17 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 18 | '$status $body_bytes_sent "$http_referer" ' 19 | '"$http_user_agent" "$http_x_forwarded_for"'; 20 | 21 | access_log /var/log/nginx/access.log main; 22 | 23 | sendfile on; 24 | #tcp_nopush on; 25 | 26 | keepalive_timeout 65; 27 | 28 | #gzip on; 29 | 30 | include /etc/nginx/conf.d/*.conf; 31 | } --------------------------------------------------------------------------------