├── .gitignore ├── LICENSE ├── README.en.md ├── README.md ├── api ├── Dockerfile ├── django_rest_framework_tutorial │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── docker-entrypoint.sh ├── manage.py ├── musics │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ └── views.py ├── requirements.txt ├── uwsgi.ini └── uwsgi_params ├── docker-compose.yml ├── my_htpasswd └── htpasswd └── nginx ├── Dockerfile ├── my_nginx.conf ├── nginx-access.example_log ├── 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.en.md: -------------------------------------------------------------------------------- 1 | # docker-django-nginx-uwsgi-postgres-tutorial 2 | 3 | Docker + Django + Nginx + uWSGI + Postgres Basic Tutorial - from nothing to something 4 | 5 | This tutorial teaches you how to setup [Django](https://www.djangoproject.com/) + [Nginx](https://nginx.org/en/) + [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) + [PostgreSQL](https://www.postgresql.org/) with Docker 📝 6 | 7 | For those who are not familiar with [Docker](https://www.docker.com/), I suggest that you read my previous tutorials first: 8 | 9 | [Docker 基本教學 - 從無到有 Docker-Beginners-Guide 教你用 Docker 建立 Django + PostgreSQL 📝](https://github.com/twtrubiks/docker-tutorial) 10 | 11 | * [Youtube Tutorial PART 1 - Docker + Django + Nginx + uWSGI + Postgres - 簡介](https://youtu.be/u4XIMTOsxJk) 12 | * [Youtube Tutorial PART 2 - Docker + Django + Nginx + uWSGI + Postgres - 原理步驟](https://youtu.be/9K4O1UuaXrU) 13 | * [Youtube Tutorial PART 3 - Docker + Django + Nginx + uWSGI + Postgres - 實戰](https://youtu.be/v7Mf9TuROnc) 14 | 15 | ## Summary 16 | 17 | ### [Docker](https://www.docker.com/) 18 | 19 | ![](https://i.imgur.com/gDcSwcs.png) 20 | 21 | I have introduced Docker before, so I won't introduce it here :stuck_out_tongue_closed_eyes: 22 | 23 | Take a look at: 24 | 25 | [Docker 基本教學 - 從無到有 Docker-Beginners-Guide 教你用 Docker 建立 Django + PostgreSQL 📝](https://github.com/twtrubiks/docker-tutorial) 26 | 27 | ### [Django](https://github.com/django/django) 28 | 29 | Take a look at: 30 | 31 | [Django 基本教學 - 從無到有 Django-Beginners-Guide 📝](https://github.com/twtrubiks/django-tutorial) 32 | 33 | [Django-REST-framework 基本教學 - 從無到有 DRF-Beginners-Guide 📝](https://github.com/twtrubiks/django-rest-framework-tutorial) 34 | 35 | For more Django examples, check out my [Github](https://github.com/twtrubiks?utf8=%E2%9C%93&tab=repositories&q=Django&type=&language=), I have just listed two of the simpler ones here :relaxed: 36 | 37 | ### [PostgreSQL](https://www.postgresql.org/) 38 | 39 | ![](https://i.imgur.com/RrNtbfz.png) 40 | 41 | ### [Nginx](https://nginx.org/en/) 42 | 43 | ![](https://i.imgur.com/AkcCtDa.png) 44 | 45 | Nginx is a type of Web Server that uses few resources and has high stability. 46 | 47 | Nginx's high stability is a result of solving the **C10K** problem. What is **C10K**? The original literature can be found here [The C10K problem](http://www.kegel.com/c10k.html). 48 | 49 | **C10K** is the Client 10000 problem. Previously, when a server receives more than 10000 concurrent connections, it may not be able to operate normally. 50 | 51 | Nginx does not natively support dynamic content, so, any dynamic content needs to be set up separately. [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) is used to facilitate the communication between Nginx and the dynamic content. 52 | 53 | Take a look at the diagram below (important) 54 | 55 | :star: the web client <-> the web server ( Nginx ) <-> unix socket <-> uWSGI <-> Django :star: 56 | 57 | You may ask me, what is uWSGI :confused:? 58 | 59 | uWSGI implements a communication protocol. You can think of it as a connector (which communicates with Django). 60 | 61 | Usually, Django will be put behind the http server ( Nginx ), so, when the server receives a request, how will it pass the data to Django? 62 | 63 | 64 | This is uWSGI's functionality :wink: 65 | 66 | So why do we still need Nginx :confused:? 67 | 68 | First, let's understand a concept, 69 | 70 | Nginx is in charge of static content (html, js, css, images, ...), uWSGI is in charge of Python's dynamic content. 71 | 72 | uWSGI does not handle static content well (it's inefficient), so, we can use Nginx to handle static content. In addition, Nginx has a lot of other benefits. 73 | 74 | * Nginx, compared to uWSGI, handles static resources better 75 | * Nginx allows cache configuration 76 | * Nginx can act as a reverse proxy 77 | * Nginx can load balance multiple connections 78 | 79 | Gentle reminder :heart: 80 | 81 | If you would like learn more about **reverse proxies**, you can take a look at [forward proxy VS reverse proxy](https://github.com/twtrubiks/docker-django-nginx-uwsgi-postgres-load-balance-tutorial#%E6%AD%A3%E5%90%91%E4%BB%A3%E7%90%86%E5%99%A8--vs-%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86%E5%99%A8) :smile: 82 | 83 | Up to this point, you may have some questions: 84 | 85 | Why can I still run Django without Nginx and uWSGI? :confused: 86 | 87 | To run Django, we usually use `python manage.py runserver` to run the server. 88 | 89 | Actually, when you run this command, Django helps you start a small http server. 90 | 91 | Of course, this is just for convenience during development. We would not do this in production ( not to mention performance :disappointed_relieved: ) 92 | 93 | Hey :confused: isn't there Gunicorn? Previously you mentioned Gunicorn in 94 | [Deploying_Django_To_Heroku_Tutorial](https://github.com/twtrubiks/Deploying_Django_To_Heroku_Tutorial) 95 | [Deploying-Flask-To-Heroku](https://github.com/twtrubiks/Deploying-Flask-To-Heroku) 96 | 97 | So why can't we use Gunicorn instead of uWSGI? 98 | 99 | The last time, when I used Gunicorn, it was because the application resided in Heroku, and it is recommended to use Gunicorn to set up a web server there. As for which is better, Gunicorn or uWSGI, I feel that it is up your use case :wink: 100 | 101 | Wait a minute, since we are talking about Nginx, isn't there also [Apache](https://httpd.apache.org/)? I heard that a lot of people use that :stuck_out_tongue_winking_eye: 102 | 103 | You may also be asking, so should I choose [Nginx](https://nginx.org/en/) or [Apache](https://httpd.apache.org/) :confused: 104 | 105 | I think that there is no best server, if the server meets your requirements, the choose it :smiley: 106 | 107 | ## Tutorial 108 | 109 | This time, I will be using Docker to set up 3 containers to separate Nginx, Django + uWSGI and Postgres 110 | 111 | My main reference is [https://uwsgi-docs.readthedocs.io/en/latest/tutorials/Django_and_nginx.html](https://uwsgi-docs.readthedocs.io/en/latest/tutorials/Django_and_nginx.html) in this tutorial. 112 | 113 | But there are some differences :smirk: 114 | 115 | The main focus this time will be on setting up of Nginx with Django + uWSGI 116 | 117 | **Nginx Section**, you can take a look at [Dockerfile](https://github.com/twtrubiks/docker-django-nginx-uswgi-postgres-tutorial/blob/master/nginx/Dockerfile) in the folder. 118 | 119 | ```Dockerfile 120 | FROM nginx:latest 121 | 122 | COPY nginx.conf /etc/nginx/nginx.conf 123 | COPY my_nginx.conf /etc/nginx/sites-available/ 124 | 125 | RUN mkdir -p /etc/nginx/sites-enabled/\ 126 | && ln -s /etc/nginx/sites-available/my_nginx.conf /etc/nginx/sites-enabled/ 127 | 128 | # RUN mkdir -p /etc/nginx/sites-enabled/\ 129 | # && ln -s /etc/nginx/sites-available/my_nginx.conf /etc/nginx/sites-enabled/\ 130 | # && rm /etc/nginx/conf.d/default.conf 131 | 132 | CMD ["nginx", "-g", "daemon off;"] 133 | ``` 134 | 135 | Let me explain the steps, 136 | 137 | First Step 138 | 139 | Copy nginx.conf to the `/etc/nginx/nginx.conf` path. 140 | 141 | (the original nginx.conf can be retrieved from the Nginx Docker container, inside the `/etc/nginx` path you can find nginx.conf) 142 | 143 | I have copied out a part of the original file [nginx_origin.conf](https://github.com/twtrubiks/docker-django-nginx-uswgi-postgres-tutorial/blob/master/nginx/nginx_origin.conf) :smiley: 144 | 145 | For nginx.conf, there are mainly two parts that needs to be modified. 146 | 147 | The first part is to change the user to root: 148 | 149 | ```conf 150 | user root; 151 | ``` 152 | 153 | The other part is: 154 | 155 | ```conf 156 | # include /etc/nginx/conf.d/*.conf; 157 | include /etc/nginx/sites-available/*; 158 | ``` 159 | 160 | Add a line `include /etc/nginx/sites-available/*;` 161 | 162 | and remove `include /etc/nginx/conf.d/*.conf;` 163 | 164 | This way the [Dockerfile](https://github.com/twtrubiks/docker-django-nginx-uswgi-postgres-tutorial/blob/master/nginx/Dockerfile) 165 | in the Nginx folder does not need the command to delete the default.conf because `include /etc/nginx/conf.d/*.conf;` is the default page that will run. 166 | 167 | But now, we need to configure it :smirk: 168 | 169 | Second Step 170 | 171 | Copy my_nginx.conf into `/etc/nginx/sites-available/` 172 | 173 | Let us pause here for a while, 174 | 175 | If you use `FROM nginx:latest` to install Nginx, you will discover that you do not have the two directories below: 176 | 177 | `/etc/nginx/sites-available/` 178 | 179 | `/etc/nginx/sites-enabled/` 180 | 181 | but don't worry, if the directories aren't there, we will add them in ( which is what the commands in the Nginx Dockerfile is doing ) 182 | 183 | But why don't we have these directories in the first place :confused: 184 | 185 | The reason is because these directories are only created when Nginx is installed with `apt-get`. 186 | 187 | Third Step 188 | 189 | sites-available This directory is actually not important, you can create a folder with a name you want too, but 190 | 191 | *sites-enabled* This directory is more important because we need to use symlinks (through the Linux command `ln`) link *sites-enabled* with *my_nginx.conf. 192 | 193 | Next, let's talk about the configuration in my_nginx.conf 194 | 195 | ```conf 196 | # the upstream component nginx needs to connect to 197 | upstream uwsgi { 198 | # server api:8001; # use TCP 199 | server unix:/docker_api/app.sock; # for a file socket 200 | } 201 | 202 | # configuration of the server 203 | server { 204 | # the port your site will be served on 205 | listen 80; 206 | # index index.html; 207 | # the domain name it will serve for 208 | # substitute your machine's IP address or FQDN 209 | server_name twtrubiks.com www.twtrubiks.com; 210 | charset utf-8; 211 | 212 | client_max_body_size 75M; # adjust to taste 213 | 214 | # Django media 215 | # location /media { 216 | # alias /docker_api/static/media; # your Django project's media files - amend as required 217 | # } 218 | 219 | location /static { 220 | alias /docker_api/static; # your Django project's static files - amend as required 221 | } 222 | 223 | location / { 224 | uwsgi_pass uwsgi; 225 | include /etc/nginx/uwsgi_params; # the uwsgi_params file you installed 226 | } 227 | 228 | } 229 | ``` 230 | 231 | Let's first look at the upstream part. 232 | 233 | It uses Unix sockets which is better than using TCP port sockets because there is less overhead. 234 | 235 | Next is `include /etc/nginx/uwsgi_params`, usually the uwsgi_params can be found in the Nginx directory `/etc/nginx`. 236 | 237 | If you really can't find it, you can copy one from [uwsgi_params](https://github.com/twtrubiks/docker-django-nginx-uswgi-postgres-tutorial/blob/master/api/uwsgi_params) (I copied out the file for everyone but if you follow my steps, you will probably have it) 238 | 239 | Next, let's talk about uwsgi_pass or what you have seen is proxy_pass. 240 | 241 | Nginx will convert the request received to uwsgi's protocol. Then, it will pass the request to Django to handle it. 242 | 243 | Then why can't we just use a proxy ( default to http protocol ) but we have to use uwsgi :confused:? 244 | 245 | The main reason is because efficiency is taken into consideration. 246 | 247 | Since we have already talked about it so much, I'll briefly explain what is a **Proxy server**. 248 | 249 | When a client from an external network sends a request, the proxy server will forward it to an internal server to handle it. Once it's done, the response goes back through the proxy server to the client in the external network. 250 | 251 | What's the benefit in this :confused:? The benefit is that it protects the internal server, preventing clients from directly attacking the internal server. 252 | 253 | Another benefit is the caching mechanism. If the client accesses similar resources, the resources can be retrieved directly from cache. 254 | 255 | Last Step 256 | 257 | Gentle reminder :heart: 258 | 259 | What is a daemon :question::question::question: 260 | 261 | Actually, it is not very hard to understand, you can think of it as a type of service :smile: 262 | 263 | If you want to learn more about daemons, you can google **linux daemon** :pencil2: 264 | 265 | Why do we use `nginx -g daemon off` to start Nginx but not `/etc/init.d/nginx start` :confused:? 266 | 267 | This question requires us to go back and understand Docker. 268 | 269 | The excerpt below is from [Docker Nginx](https://hub.docker.com/_/nginx/) 270 | 271 | ***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)!*** 272 | 273 | Simply put, it is to keep the Nginx service running. If not, the container will exit. 274 | 275 | **Django + uWSGI Section**, you can take a look at the [Dockerfile](https://github.com/twtrubiks/docker-django-nginx-uswgi-postgres-tutorial/blob/master/api/Dockerfile) in the api folder 276 | 277 | It it mostly quite simple, but there I would like to point out one thing: 278 | 279 | Sometimes when we `pip install`, it runs very slowly. 280 | 281 | In these situations, we can add a `-i` option to change the index source, making it run a bit faster :grin: 282 | 283 | Next, I'll explain uwsgi.ini, inside of which are some configuration files 284 | 285 | ```ini 286 | [uwsgi] 287 | 288 | # http=0.0.0.0:8000 289 | socket=app.sock 290 | master=true 291 | # maximum number of worker processes 292 | processes=4 293 | threads=2 294 | # Django's wsgi file 295 | module=django_rest_framework_tutorial.wsgi:application 296 | 297 | # chmod-socket=664 298 | # uid=www-data 299 | # gid=www-data 300 | 301 | # clear environment on exit 302 | vacuum = true 303 | ``` 304 | 305 | Communication to Nginx is done through the socket file ( app.sock ). The uid and gid parts are permissions. 306 | 307 | You can take a look at the article below. The article includes why we do not use root permissions. 308 | 309 | [Things to know (best practices and 「issues」) READ IT !!!](http://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html) 310 | 311 | I decided to use root anyways because without root, there will be permission errors which I finally found my answer [here](https://stackoverflow.com/questions/18480201/ubuntu-nginx-emerg-bind-to-0-0-0-080-failed-13-permission-denied) 312 | 313 | *the socket API bind() to a port less than 1024, such as 80 as your title mentioned, need root access.* 314 | 315 | The simpler solution is to run with root :smile: 316 | 317 | Lastly, I used `docker-compose.yml` to manage my containers. 318 | 319 | See my [docker-compose.yml](https://github.com/twtrubiks/docker-django-nginx-uswgi-postgres-tutorial/blob/master/docker-compose.yml) 320 | 321 | ## Steps to run 322 | 323 | Run `docker-compose up` and watch the magic happen. 324 | 325 | You will see something like this: 326 | 327 | ![](https://i.imgur.com/4WPac2V.png) 328 | 329 | ![](https://i.imgur.com/I67WDJU.png) 330 | 331 | If you then see something like the below, then it has run successfully. 332 | 333 | ![](https://i.imgur.com/WwRLm4C.png) 334 | 335 | ![](https://i.imgur.com/G28IGca.png) 336 | 337 | Next, go to [http://localhost:8080/api/music/](http://localhost:8080/api/music/) 338 | 339 | If you immediately see something like the image below, it means that you have completed a small step. 340 | 341 | Seeing this is normal, because we still need to migrate. 342 | 343 | ![](https://i.imgur.com/d0jlMo9.png) 344 | 345 | The terminal output is also fine ( though it is easy to get stuck here :sweat_smile: ) 346 | 347 | ![](https://i.imgur.com/RBW8eQt.png) 348 | 349 | Next, open another terminal and go into the api ( Django + uWSGI ) container. 350 | 351 | You can find the steps in the previous [docker-tutorial-指令介紹](https://github.com/twtrubiks/docker-tutorial#指令介紹), 352 | 353 | You can also use other GUI applications like the [previously introduced portainer](https://github.com/twtrubiks/docker-tutorial#其他管理-docker-gui-的工具) 354 | 355 | ```cmd 356 | docker exec -it bash 357 | ``` 358 | 359 | ```cmd 360 | python manage.py makemigrations musics 361 | python manage.py migrate 362 | python manage.py createsuperuser 363 | ``` 364 | 365 | ![](https://i.imgur.com/haHcokf.png) 366 | 367 | This time, we need to run a different command 368 | 369 | ```cmd 370 | python manage.py collectstatic 371 | ``` 372 | 373 | This takes all the static files in Django and collates them into the static folder. 374 | 375 | ![](https://i.imgur.com/zaz2bYX.png) 376 | 377 | Next, you can go to [http://localhost:8080/api/music/](http://localhost:8080/api/music/), then you will see the normal page :smile: 378 | 379 | ![](https://i.imgur.com/EybsFZ3.png) 380 | 381 | Why do we need to do this step? 382 | 383 | The purpose of the step is to pass all the static content to Nginx to process. In my_nginx.conf, you will notice that we pointed Nginx to the `/docker_api/static` path. 384 | 385 | As said above, Nginx is in charge of all the static content ( html, css, images, ...... ) while uWSGI is in charge of Python's dynamic content. 386 | 387 | If you are interested to try it out, use Django + uWSGI without Nginx. If you do this, it will function normally but you will notice that the css, images and others cannot be retrieved, as shown below 388 | 389 | ![](https://i.imgur.com/btiI68s.png) 390 | 391 | Because uWSGI does not handle static content well :sob: 392 | 393 | Though this can be resolved. Take a look at [https://uwsgi-docs.readthedocs.io/en/latest/StaticFiles.html](https://uwsgi-docs.readthedocs.io/en/latest/StaticFiles.html) 394 | 395 | but I recommend that you use Nginx, because it can do more things :smiley: 396 | 397 | ## Final Result 398 | 399 | Go to [http://localhost:8080/api/music/](http://localhost:8080/api/music/) 400 | 401 | ![](https://i.imgur.com/jl43jST.png) 402 | 403 | ![](https://i.imgur.com/Fw6LjbE.png) 404 | 405 | ## `hosts` Configuration and Finding Your IP Address 406 | 407 | Edit the `hosts` configuration file 408 | 409 | Windows 410 | 411 | `hosts` is located at 412 | 413 | > C:\WINDOWS\system32\drivers\etc\hosts 414 | 415 | ![](https://i.imgur.com/Q6lZyK0.png) 416 | 417 | You may need permissions to save the file. 418 | 419 | MAC 420 | 421 | `hosts` is located at 422 | 423 | > sudo vi /etc/hosts 424 | 425 | Finding your IP address 426 | 427 | Windows 428 | 429 | ```cmd 430 | ipconfig 431 | ``` 432 | 433 | ![](https://i.imgur.com/sPOqIxM.png) 434 | 435 | MAC 436 | 437 | ```cmd 438 | ifconfig 439 | ``` 440 | 441 | ![](https://i.imgur.com/BOs5BwZ.png) 442 | 443 | Let's say your ip address is 192.168.1.103, then those in the same network as you (intranet), can connect to you through this ip address. 444 | 445 | As for the editing the `hosts` configuration file part, we can just go to [http://twtrubiks.com:8080/api/music/](http://twtrubiks.com:8080/api/music/) 446 | 447 | ![](https://i.imgur.com/efEqLd0.png) 448 | 449 | ## Introducing Supervisor 450 | 451 | [Supervisor](http://supervisord.org/) 452 | 453 | is a type of process management tool. Using it, you can easily start, stop, restart and monitor one or many processes. 454 | 455 | For example, if a process stalls, once Supervisor notices, it will automatically restart the process. No need to write any programs ( such as your own shell programs ) to control it. 456 | 457 | Now, you may ask me: 458 | 459 | Should I use Supervisor :confused:? 460 | 461 | When do I use Supervisor? 462 | 463 | You use Supervisor when you need to start multiple independent processes in a container. 464 | 465 | Let me give you an example, let's say you create a container with Nginx + uWSGI + Django, all in the same container. Then, in this case, Supervisor will be suitable. 466 | 467 | Though, if you are using Docker, having Nginx and uWSGI + Django separate is better ( meaning, separate them in two different containers ) 468 | 469 | Then, use docker-compose to manage the containers; which is this example's method. 470 | 471 | Then you will ask, how do I manage containers which unexpectedly exit :confused:? 472 | 473 | In this case, you can take a look at the [docker-compose.yml](https://github.com/twtrubiks/docker-django-nginx-uswgi-postgres-tutorial/blob/master/docker-compose.yml) which uses `restart=always` to solve this problem. It will help you restart the container when the container exits unexpectedly :relaxed: 474 | 475 | ## Conclusion 476 | 477 | This also my first time setting up Django + Nginx + uWSGI + Postgres, during which I had to mess around with the configuration for a very long time :scream:. But, I wholeheartedly recommend Docker. 478 | 479 | Using Docker was really fun, even when I brake the project, I can just start over again. It's fast and, through this exercise, you will see that Nginx actually has a lot of features which you can play with such as the Load Balancer. Through which you can better understand servers; I myself understood a lot from it. 480 | 481 | In short, I recommend that everyone get their hands dirty, follow my footsteps and play with it. I believe that more or less everyone will learn something. 482 | 483 | I am also new to Docker, if I have done anything wrong, please let me know, I will make the necessary changes :blush: 484 | 485 | If you are still learning to read, read more :satisfied: 486 | 487 | * [實戰 Docker + Django + Nginx + uWSGI + Postgres - Load Balance 📝](https://github.com/twtrubiks/docker-django-nginx-uwsgi-postgres-load-balance-tutorial) 488 | * [Docker Swarm 基本教學 - 從無到有 Docker-Swarm-Beginners-Guide📝](https://github.com/twtrubiks/docker-swarm-tutorial) 489 | 490 | ## Environment 491 | 492 | * Mac 493 | * Python 3.8.2 494 | * windows 10 495 | * Liunx 496 | 497 | ## Reference 498 | 499 | * [https://docs.docker.com/](https://docs.docker.com/) 500 | * [uwsgi-docs](https://uwsgi-docs.readthedocs.io/en/latest/tutorials/Django_and_nginx.html) 501 | 502 | ## Donation 503 | 504 | This document is the result of my own internalization after researching. If it has helped you, and you would like to encourage me, you're welcome to buy me a cup of coffee :laughing: 505 | 506 | ![alt tag](https://i.imgur.com/LRct9xa.png) 507 | 508 | [Sponsor Me](https://payment.opay.tw/Broadcaster/Donate/9E47FDEF85ABE383A0F5FC6A218606F8) 509 | 510 | ## License 511 | 512 | MIT license 513 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-django-nginx-uwsgi-postgres-tutorial 2 | 3 | [English version - README.en.md](https://github.com/twtrubiks/docker-django-nginx-uwsgi-postgres-tutorial/blob/master/README.en.md) 4 | 5 | Docker + Django + Nginx + uWSGI + Postgres 基本教學 - 從無到有 6 | 7 | 教你用 [Docker](https://www.docker.com/) 建立 [Django](https://www.djangoproject.com/) + [Nginx](https://nginx.org/en/) + [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) + [PostgreSQL](https://www.postgresql.org/) 📝 8 | 9 | 建議對 [Docker](https://www.docker.com/) 還不熟的朋友,可以先參考我之前寫的 10 | 11 | [Docker 基本教學 - 從無到有 Docker-Beginners-Guide 教你用 Docker 建立 Django + PostgreSQL 📝](https://github.com/twtrubiks/docker-tutorial) 12 | 13 | * [Youtube Tutorial PART 1 - Docker + Django + Nginx + uWSGI + Postgres - 簡介](https://youtu.be/u4XIMTOsxJk) 14 | * [Youtube Tutorial PART 2 - Docker + Django + Nginx + uWSGI + Postgres - 原理步驟](https://youtu.be/9K4O1UuaXrU) 15 | * [Youtube Tutorial PART 3 - Docker + Django + Nginx + uWSGI + Postgres - 實戰](https://youtu.be/v7Mf9TuROnc) 16 | 17 | * [Youtube Tutorial - 透過 Nginx Log 分析 PV UV](https://youtu.be/mUyDVVX6OD4) - [文章快速連結](https://github.com/twtrubiks/docker-django-nginx-uwsgi-postgres-tutorial#%E9%80%8F%E9%81%8E-nginx-log-%E5%88%86%E6%9E%90-pv-uv) 18 | 19 | * [Youtube Tutorial - NGINX 教學 - auth basic](https://youtu.be/zWODI3YHb2Y) - [文章快速連結](https://github.com/twtrubiks/docker-django-nginx-uwsgi-postgres-tutorial#%E8%A8%AD%E5%AE%9A-auth_basic) 20 | 21 | ## 簡介 22 | 23 | ### [Docker](https://www.docker.com/) 24 | 25 | ![](https://i.imgur.com/gDcSwcs.png) 26 | 27 | 之前介紹過了,這邊就不在介紹 :stuck_out_tongue_closed_eyes: 28 | 29 | 請參考 30 | 31 | [Docker 基本教學 - 從無到有 Docker-Beginners-Guide 教你用 Docker 建立 Django + PostgreSQL 📝](https://github.com/twtrubiks/docker-tutorial) 32 | 33 | ### [Django](https://github.com/django/django) 34 | 35 | 請參考 36 | 37 | [Django 基本教學 - 從無到有 Django-Beginners-Guide 📝](https://github.com/twtrubiks/django-tutorial) 38 | 39 | [Django-REST-framework 基本教學 - 從無到有 DRF-Beginners-Guide 📝](https://github.com/twtrubiks/django-rest-framework-tutorial) 40 | 41 | 更多 Django 的範例可以參考我的 [Github](https://github.com/twtrubiks?utf8=%E2%9C%93&tab=repositories&q=Django&type=&language=),這裡我就列出比較基本的兩篇就好:relaxed: 42 | 43 | ### [PostgreSQL](https://www.postgresql.org/) 44 | 45 | ![](https://i.imgur.com/RrNtbfz.png) 46 | 47 | ### [Nginx](https://nginx.org/en/) 48 | 49 | ![](https://i.imgur.com/AkcCtDa.png) 50 | 51 | Nginx 是一種 Web Server,使用資源少且穩定性高,穩定性高這部分可參考 52 | 53 | Nginx 解決了 **C10K** 問題,什麼是 **C10K**? 原文可參考 [The C10K problem](http://www.kegel.com/c10k.html) , 54 | 55 | **C10K** 就是 Client 10000 的問題,在過去,如果同時連接到 Server 的 Client 56 | 57 | 端數超過 10000 中,可能無法正常提供服務。 58 | 59 | Ngnix 本身沒辦法處理動態內容,所以必須另外設定 [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) 來處理之間的互動 60 | 61 | ,參考下方流程 ( 重要 ) 62 | 63 | :star: the web client <-> the web server ( Nginx ) <-> unix socket <-> uWSGI <-> Django :star: 64 | 65 | 你可能會問我,uWSGI 這個是什麼 :confused: 66 | 67 | uWSGI 是一種通信協議,可以把它想成是一種接口 ( 和 Django 進行溝通 ), 68 | 69 | 通常 Django 程式會放在 http server( Nginx )上,那當 server 接收到 70 | 71 | request 時,該怎麼將這些數據傳遞(轉換)給 Django 呢 ? 72 | 73 | 這就是 uWSGI 的功能 :wink: 74 | 75 | 那為什麼還需要 Nginx 呢 :confused: 76 | 77 | 先了解一個觀念, 78 | 79 | Nginx 負責靜態內容(html js css 圖片...... ),uWSGI 負責 Python 的動態內容。 80 | 81 | uWSGI 對於靜態內容處理的並不是很好( 效能差 ),所以我們可以透過 82 | 83 | Nginx 來處理靜態內容,而且使用 Nginx 還有很多好處, 84 | 85 | * Nginx 比起 uWSGI 能更好地處理靜態資源 86 | * Nginx 可以設定 Cache 機制 87 | * Nginx 可以設定 反向代理器 88 | * Nginx 可以進行多台機器的負載平衡( Load balance ) 89 | 90 | 溫馨小提醒:heart: 91 | 92 | 如果你想更進一步的了解**反向代理器**,可參考 [正向代理器 VS 反向代理器](https://github.com/twtrubiks/docker-django-nginx-uwsgi-postgres-load-balance-tutorial#%E6%AD%A3%E5%90%91%E4%BB%A3%E7%90%86%E5%99%A8--vs-%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86%E5%99%A8) 的說明 :smile: 93 | 94 | 看到這邊你可能會問我? 95 | 96 | 那為什麼我在本機開發時,都不需要有 Nginx 以及 uWSGI 就可以執行呢 :confused: 97 | 98 | 當你在開發 Django 時,我們通常都是用 `python manage.py runserver` 99 | 100 | 去執行,當你執行這段指令時,其實他就是幫你建立一個小型的 http server , 101 | 102 | 當然,這只是開發方便,正式環境是不會這樣使用的( 更何況效能的部份 :disappointed_relieved: ) 103 | 104 | 疑 :confused: 好像有 Gunicorn 這個東西,之前有講過 Gunicorn ,可參考 105 | 106 | [Deploying_Django_To_Heroku_Tutorial](https://github.com/twtrubiks/Deploying_Django_To_Heroku_Tutorial) 107 | [Deploying-Flask-To-Heroku](https://github.com/twtrubiks/Deploying-Flask-To-Heroku) 108 | 109 | 那為什麼不用 Gunicorn,要用 uWSGI 呢? 110 | 111 | 那時候會使用 Gunicorn,是因為在 Heroku 裡,官方建議使用 Gunicorn 來 112 | 113 | 啟動 web server,至於 Gunicorn 和 uWSGI 哪個比較好,我覺得要依照自 114 | 115 | 己的使用情境下去選擇 :wink: 116 | 117 | 等等,既然都講到了 Nginx,不是還有 [Apache](https://httpd.apache.org/),聽說那個好像很多人在用 :stuck_out_tongue_winking_eye: 118 | 119 | 可能有人會問,那我要選 [Nginx](https://nginx.org/en/) 還是 [Apache](https://httpd.apache.org/) :confused: 120 | 121 | 我認為沒有哪個 Server 最好,重點在你的需求下,哪個 Server 最符合你的情境(需求), 122 | 123 | 你就選他 :smiley: 124 | 125 | ## 教學 126 | 127 | 這次我將利用 Docker 建立 3 個容器( Containers ),分別為 Nginx、Django + uWSGI 、Postgres 128 | 129 | 我主要是參考 [https://uwsgi-docs.readthedocs.io/en/latest/tutorials/Django_and_nginx.html](https://uwsgi-docs.readthedocs.io/en/latest/tutorials/Django_and_nginx.html) 這篇教學, 130 | 131 | 但些許部分不太一樣 :smirk: 132 | 133 | 這次的重點會放在 Nginx 以及 Django + uWSGI 設定的部份, 134 | 135 | **Nginx 的部份**,可參考 Nginx 資夾中的 [Dockerfile](https://github.com/twtrubiks/docker-django-nginx-uswgi-postgres-tutorial/blob/master/nginx/Dockerfile) 136 | 137 | ```Dockerfile 138 | FROM nginx:latest 139 | 140 | COPY nginx.conf /etc/nginx/nginx.conf 141 | COPY my_nginx.conf /etc/nginx/sites-available/ 142 | 143 | RUN mkdir -p /etc/nginx/sites-enabled/\ 144 | && ln -s /etc/nginx/sites-available/my_nginx.conf /etc/nginx/sites-enabled/ 145 | 146 | # RUN mkdir -p /etc/nginx/sites-enabled/\ 147 | # && ln -s /etc/nginx/sites-available/my_nginx.conf /etc/nginx/sites-enabled/\ 148 | # && rm /etc/nginx/conf.d/default.conf 149 | 150 | CMD ["nginx", "-g", "daemon off;"] 151 | ``` 152 | 153 | 解釋一下裡面的步驟, 154 | 155 | 第一步 156 | 157 | 先將 nginx.conf 複製到 `/etc/nginx/nginx.conf` 的路徑, 158 | 159 | ( 原始的 nginx.conf 可以從 Docker 的 Nginx 容器中取得,在 `/etc/nginx` 路徑下取得 nginx.conf ) 160 | 161 | 我有複製一份原始的出來 [nginx_origin.conf](https://github.com/twtrubiks/docker-django-nginx-uswgi-postgres-tutorial/blob/master/nginx/nginx_origin.conf) :smiley: 162 | 163 | nginx.conf 主要是修改兩個部分, 164 | 165 | 一部分是將 user 從 nginx 修改為 root, 166 | 167 | ```conf 168 | user root; 169 | ``` 170 | 171 | 另一部份是 172 | 173 | ```conf 174 | # include /etc/nginx/conf.d/*.conf; 175 | include /etc/nginx/sites-available/*; 176 | ``` 177 | 178 | 增加一行 `include /etc/nginx/sites-available/*;` 179 | 180 | 並且將 `include /etc/nginx/conf.d/*.conf;` 這行註解掉, 181 | 182 | 這樣在 Nginx 資料夾中的 [Dockerfile](https://github.com/twtrubiks/docker-django-nginx-uswgi-postgres-tutorial/blob/master/nginx/Dockerfile) 就不用再執行刪除 default.conf 的指令, 183 | 184 | 因為 `include /etc/nginx/conf.d/*.conf;` 它是默認會跑的頁面, 185 | 186 | 但我們現在要設定自己的 :smirk: 187 | 188 | 第二步 189 | 190 | 將 my_nginx.conf 複製到 `/etc/nginx/sites-available/` 裡面, 191 | 192 | 這邊我們先暫停一下, 193 | 194 | 如果你是用 `FROM nginx:latest` 的方式安裝 Nginx,你會發現你沒有以下兩個路徑 195 | 196 | `/etc/nginx/sites-available/` 197 | 198 | `/etc/nginx/sites-enabled/` 199 | 200 | 但不要擔心,沒有我們就自己建立 ( 也就是 Nginx 資料夾中的 Dockerfile 所執行的指令 ) , 201 | 202 | 但為什麼我們沒有這些路徑呢 :confused: 203 | 204 | 原因是這些默認路徑似乎是要用 `apt-get` 的方式安裝 Nginx 才會有默認的路徑。 205 | 206 | 第三步 207 | 208 | sites-available 這個資料夾其實不重要,你也可以取名自己喜歡的資料夾,但 209 | 210 | *sites-enabled* 這個資料夾就比較重要了,因為我們要使用 Symlink 的方法 211 | 212 | (透過 Linux 中的 `ln` 指令 )將 *sites-enabled* 以及 *my_nginx.conf* 連結起來。 213 | 214 | 接著來說說 my_nginx.conf 裡面的設定 215 | 216 | ```conf 217 | # the upstream component nginx needs to connect to 218 | upstream uwsgi { 219 | # server api:8001; # use TCP 220 | server unix:/docker_api/app.sock; # for a file socket 221 | } 222 | 223 | # configuration of the server 224 | server { 225 | # the port your site will be served on 226 | listen 80; 227 | # index index.html; 228 | # the domain name it will serve for 229 | # substitute your machine's IP address or FQDN 230 | server_name twtrubiks.com www.twtrubiks.com; 231 | charset utf-8; 232 | 233 | client_max_body_size 75M; # adjust to taste 234 | 235 | # Django media 236 | # location /media { 237 | # alias /docker_api/static/media; # your Django project's media files - amend as required 238 | # } 239 | 240 | location /static { 241 | alias /docker_api/static; # your Django project's static files - amend as required 242 | } 243 | 244 | location / { 245 | uwsgi_pass uwsgi; 246 | include /etc/nginx/uwsgi_params; # the uwsgi_params file you installed 247 | } 248 | 249 | } 250 | ``` 251 | 252 | 也可以透過以下的指令初步測試 nginx 是否設定正確, 253 | 254 | ```cmd 255 | nginx -t 256 | # or 257 | nginx -T 258 | ``` 259 | 260 | `-t` 代表 test configuration and exit. 261 | 262 | `-T` 代表 test configuration, dump it and exit. 263 | 264 | 先來看 upstream 的部份,使用 Unix sockets 的方式, 265 | 266 | 會比使用 267 | TCP port socket 的方式還要好,因為開銷比較小。 268 | 269 | 再來是 `include /etc/nginx/uwsgi_params` 的部份,一般來說, 270 | 271 | Nginx 的路徑 `/etc/nginx/` 底下可以找的到 uwsgi_params,如果 272 | 273 | 真的找不到,可以到這裡複製進去 [uwsgi_params](https://github.com/twtrubiks/docker-django-nginx-uswgi-postgres-tutorial/blob/master/api/uwsgi_params)。 274 | 275 | (我複製出來一份給大家,如果你照按我的步驟基本上都是會有) 276 | 277 | 再來需要說明一下 uwsgi_pass 這個東西,或是說你可能看過的是 proxy_pass, 278 | 279 | Nginx 會把接收到的 request 依照 uwsgi 協議轉換,然後再轉發給 Django 處理, 280 | 281 | 那為什麼不使用 proxy( 預設是 http 協議 )就好,要特別使用 uwsgi :confused: 282 | 283 | 主要是效能上的考量。 284 | 285 | 既然都講到了這邊了,簡單解釋一下什麼是 **Proxy server**, 286 | 287 | 一般稱為代理伺服器,當外部網路的使用者送出一個 request 時,Proxy server 會 288 | 289 | 將這個 request 轉送到內部網路的 server 上處理,處理完之後,再透過 Proxy server 290 | 291 | 將 resopnse 回傳給外部網路的使用者。 292 | 293 | 這樣做有什麼好處呢 :confused: 好處是可以保護內部 server 的安全,避免使用者可以直接 294 | 295 | 對 server 進行攻擊,其他好處像是還有可以做 Cache 機制,如果使用者再次存取相同的資 296 | 297 | 料,就可以直接從 Cache 中取出。 298 | 299 | 最後一步, 300 | 301 | 溫馨小提醒:heart: 302 | 303 | 什麼是 daemon :question::question::question: 304 | 305 | 其實不用把他想的太難,簡單把他想成是一種 service 即可:smile: 306 | 307 | 如果想更深入的了解 daemon,請 google **linux daemon** :pencil2: 308 | 309 | 為什麼要使用 `nginx -g daemon off` 的方式啟動 Nginx, 310 | 311 | 而不用一般的 `/etc/init.d/nginx start` 方式啟動呢 :confused: 312 | 313 | 這問題就必須回到 Docker 中去瞭解, 314 | 315 | 以下為 [Docker Nginx](https://hub.docker.com/_/nginx/) 的說明 316 | 317 | ***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)!*** 318 | 319 | 簡單來說,就是要讓 Nginx 一直保持服務,否則 Container 會退出並且停止。 320 | 321 | **Django + uWSGI 的部份**,可參考 api 資料夾裡面的 [Dockerfile](https://github.com/twtrubiks/docker-django-nginx-uswgi-postgres-tutorial/blob/master/api/Dockerfile), 322 | 323 | 裡面基本上很簡單,但有一個想提一下,有時候我們 324 | `pip install` 的時候很慢, 325 | 326 | 這時候可以考慮加個 `-i` 去改變它的來源,讓下載快一點 :grin: 327 | 328 | 接著說明 uwsgi.ini,裡面是一些設定檔 329 | 330 | ```ini 331 | [uwsgi] 332 | 333 | # http=0.0.0.0:8000 334 | socket=app.sock 335 | master=true 336 | # maximum number of worker processes 337 | processes=4 338 | threads=2 339 | # Django's wsgi file 340 | module=django_rest_framework_tutorial.wsgi:application 341 | 342 | # chmod-socket=664 343 | # uid=www-data 344 | # gid=www-data 345 | 346 | # clear environment on exit 347 | vacuum = true 348 | ``` 349 | 350 | 透過 socket file ( app.sock ) 和 Nginx 溝通,uid 和 gid 則是權限的部份, 351 | 352 | 可參考下面這篇文章說明,裡面有提到不要用 root 權限, 353 | 354 | [Things to know (best practices and 「issues」) READ IT !!!](http://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html) 355 | 356 | 我最後還是選擇使用 root 下去執行,原因是如果沒有使用 root ,會出現權限錯誤, 357 | 358 | 最後我在 [這邊](https://stackoverflow.com/questions/18480201/ubuntu-nginx-emerg-bind-to-0-0-0-080-failed-13-permission-denied) 找到答案, 359 | 360 | *the socket API bind() to a port less than 1024, such as 80 as your title mentioned, need root access.* 361 | 362 | 比較簡單的解法就是使用 root 執行:smile: 363 | 364 | 最後就是使用 `docker-compose.yml` 管理這些 Container 了, 365 | 366 | 可直接參考 [docker-compose.yml](https://github.com/twtrubiks/docker-django-nginx-uswgi-postgres-tutorial/blob/master/docker-compose.yml) 367 | 368 | ## 執行步驟 369 | 370 | 直接執行 `docker-compose up` 見證奇蹟 371 | 372 | 你會看到類似的圖 373 | 374 | ![](https://i.imgur.com/4WPac2V.png) 375 | 376 | ![](https://i.imgur.com/I67WDJU.png) 377 | 378 | 如果再看到類似下圖就代表成功了 379 | 380 | ![](https://i.imgur.com/WwRLm4C.png) 381 | 382 | ![](https://i.imgur.com/G28IGca.png) 383 | 384 | 接著瀏覽 [http://localhost/](http://localhost/) 385 | 386 | 如果你順利看到以下畫面代表成功一小步, 387 | 388 | ![](https://i.imgur.com/196wOkr.png) 389 | 390 | 接著再瀏覽 [http://localhost/api/musics/](http://localhost/api/musics/) 391 | 392 | ![](https://i.imgur.com/2QGKeex.png) 393 | 394 | 看到這些都是正常的,因為我們還必須 migrate。 395 | 396 | terminal 輸出也沒有任何問題 ( 雖然這裡很容易卡關 :sweat_smile: ) 397 | 398 | ![](https://i.imgur.com/RBW8eQt.png) 399 | 400 | 接著開啟另一個 terminal,進入 api ( Django + uWSGI ) 的容器, 401 | 402 | 指令可參考之前的 [docker-tutorial-指令介紹](https://github.com/twtrubiks/docker-tutorial#指令介紹), 403 | 404 | 也可以使用其他 GUI 工具 [之前介紹的 portainer](https://github.com/twtrubiks/docker-tutorial#其他管理-docker-gui-的工具) 405 | 406 | ```cmd 407 | docker exec -it bash 408 | ``` 409 | 410 | ```cmd 411 | python manage.py makemigrations musics 412 | python manage.py migrate 413 | python manage.py createsuperuser 414 | ``` 415 | 416 | ![](https://i.imgur.com/haHcokf.png) 417 | 418 | 這次我們要多執行一個指令 419 | 420 | ```cmd 421 | python manage.py collectstatic 422 | ``` 423 | 424 | 將 Django 中的 static files 收集起來,變成 static folder 425 | 426 | ![](https://i.imgur.com/zaz2bYX.png) 427 | 428 | 接著可以再瀏覽 [http://localhost/api/musics/](http://localhost/api/musics/), 429 | 430 | 你就會發現正常顯示了 :smile: 431 | 432 | ![](https://i.imgur.com/eb7O0g8.png) 433 | 434 | 為什麼我們要執行這步驟? 435 | 436 | 主要是把這些靜態內容交給 Nginx 去處理,在 my_nginx.conf 中, 437 | 438 | 可以發現我們將路徑指到 `/docker_api/static`。 439 | 440 | 前面有說過, 441 | 442 | Nginx 負責靜態內容( html css 圖片...... ),uWSGI 負責 Python 的動態內容。 443 | 444 | 如果你有興趣可以試試看,使用 Django + uWSGI 不使用 Nginx,這樣依然 445 | 446 | 可以正常執行,但你會發現你的 css 圖片 那些全部會抓不到,如下圖, 447 | 448 | ![](https://i.imgur.com/rgPfYeT.png) 449 | 450 | 因為 uWSGI 本身對處理靜態的內容不是很好 :sob: 451 | 452 | 雖然可以解決,可參考 [https://uwsgi-docs.readthedocs.io/en/latest/StaticFiles.html](https://uwsgi-docs.readthedocs.io/en/latest/StaticFiles.html), 453 | 454 | 但建議使用 Nginx,可以做的事情也比較多:smiley: 455 | 456 | ## 執行畫面 457 | 458 | 瀏覽 [http://localhost/api/musics/](http://localhost/api/musics/) 459 | 460 | ![](https://i.imgur.com/z0KZWEp.png) 461 | 462 | ![](https://i.imgur.com/szUTVAx.png) 463 | 464 | ## 即時監控 Nginx 網頁狀態 465 | 466 | * [Youtube Tutorial - NGINX 教學 - auth basic](https://youtu.be/zWODI3YHb2Y) 467 | 468 | 打開 stub_status 模組, 請參考 [my_nginx.conf](https://github.com/twtrubiks/docker-django-nginx-uwsgi-postgres-tutorial/blob/master/nginx/my_nginx.conf), 469 | 470 | ```conf 471 | location /nginx/status { 472 | # 啟用 stub_status 473 | stub_status on; 474 | 475 | # 關閉/啟用 log 476 | # access_log /usr/local/nginx/logs/status.log; 477 | access_log off; 478 | 479 | auth_basic "NginxStatus"; 480 | 481 | # 限制可存取的 IP 482 | # allow 127.0.0.1; 483 | # deny all; 484 | } 485 | ``` 486 | 487 | 目前 nginx 的連接狀況 488 | 489 | ![](https://i.imgur.com/GWysybq.png) 490 | 491 | 也可以設定只允許哪些 ip 訪問此頁面, 如果不在 ip 內會出現拒絕403 492 | 493 | ![](https://i.imgur.com/iFZF8Yh.png) 494 | 495 | ### 設定 auth_basic 496 | 497 | 主要加入 `auth_basic` 和 `auth_basic_user_file` 498 | 499 | 文件可參考 [Module ngx_http_auth_basic_module](http://nginx.org/en/docs/http/ngx_http_auth_basic_module.html) 500 | 501 | ```conf 502 | location /nginx/status { 503 | # 啟用 stub_status 504 | stub_status on; 505 | 506 | # 關閉/啟用 log 507 | # access_log /usr/local/nginx/logs/status.log; 508 | access_log off; 509 | 510 | auth_basic "NginxStatus"; 511 | auth_basic_user_file /my_htpasswd/htpasswd; 512 | 513 | # 限制可存取的 IP 514 | # allow 127.0.0.1; 515 | # deny all; 516 | } 517 | ``` 518 | 519 | 建立一個 htpasswd 檔案, 檔案內容如下 520 | 521 | ```text 522 | # comment 523 | name1:password1 524 | name2:password2:comment 525 | name3:password3 526 | ``` 527 | 528 | 注意, 密碼需要使用 openssl 產生, 529 | 530 | 例如, 我的密碼是 123 531 | 532 | ```cmd 533 | ❯ openssl passwd 123 534 | 8uxCGNPhjFqiw 535 | ``` 536 | 537 | 然後 htpasswd 檔案, 填入 538 | 539 | ```text 540 | # comment 541 | user1:8uxCGNPhjFqiw:123 542 | ``` 543 | 544 | 重新啟動 nginx, 就會發現要輸入帳密才能觀看, 545 | 546 | ![](https://i.imgur.com/LKFcUGz.png) 547 | 548 | ## `hosts` 設定檔 以及 查詢內網 ip 549 | 550 | 修改 `hosts` 設定檔 551 | 552 | Windows 553 | 554 | `hosts` 路徑在 555 | 556 | > C:\WINDOWS\system32\drivers\etc\hosts 557 | 558 | ![](https://i.imgur.com/Q6lZyK0.png) 559 | 560 | 儲存時可能會要求權限。 561 | 562 | MAC 563 | 564 | `hosts` 路徑在 565 | 566 | > sudo vi /etc/hosts 567 | 568 | 查詢內網 ip 569 | 570 | Windows 571 | 572 | ```cmd 573 | ipconfig 574 | ``` 575 | 576 | ![](https://i.imgur.com/sPOqIxM.png) 577 | 578 | MAC 579 | 580 | ```cmd 581 | ifconfig 582 | ``` 583 | 584 | ![](https://i.imgur.com/BOs5BwZ.png) 585 | 586 | 假如你看到的是 192.168.1.103 好了,這樣只要在相同的網路下(內網),你就可以 587 | 588 | 透過這 ip 直接連到你的網站 。 589 | 590 | 還有剛剛教大家修改 `hosts` 設定檔的部分, 591 | 592 | 我們可以直接瀏覽 [http://twtrubiks.com/api/musics/](http://twtrubiks.com/api/musics/) 593 | 594 | ![](https://i.imgur.com/ufKRO9a.png) 595 | 596 | ## 介紹 Supervisor 597 | 598 | [Supervisor](http://supervisord.org/) 599 | 600 | 是一種管理 process 的工具,透過它可以很方便的啟動、停止、重啟以及監控一個 601 | 602 | 或多個 process。假如某個 process 掛了,當 Supervisor 發現時,會自動將他再重新 603 | 604 | 啟動,不需要寫任何程式 ( 不需要再自己寫 shell 來控制 ) 605 | 606 | 這時候你一定又會問我? 這樣我要用它嗎 :confused: 607 | 608 | 哪時候需要使用 supervisor ? 609 | 610 | 當需要在同一個 container 中啟動多個獨立的 processes 時,你就適合使用 supervisor, 611 | 612 | 舉個例子,假如你在同一個 613 | container 中 ,像是 Nginx + uWSGI + Django 都在同一個 614 | 615 | container 時,你就適合使用 supervisor。 616 | 617 | 不過,如果是採用 Docker,一般會建議將 Nginx 和 uWSGI + Django 獨立出來比較好 618 | 619 | ( 也就是分成兩個 container),然後用 docker-compose 的方法管理多個 container, 620 | 621 | 也就是本範例的作法。 622 | 623 | 那你會問 ? 那我要如何管理 container 意外終止退出呢 :confused: 624 | 625 | 這時候可以參考 [docker-compose.yml](https://github.com/twtrubiks/docker-django-nginx-uswgi-postgres-tutorial/blob/master/docker-compose.yml) ,利用 `restart=always` 解決,他會在意外終止 626 | 627 | 時幫你重新啟動 :relaxed: 628 | 629 | ## CORS 踩雷分享 630 | 631 | * [Youtube Tutorial - Django + Nginx + uWSGI CORS 踩雷分享](https://youtu.be/WY2zCVfvu1M) 632 | 633 | 如果不了解 CORS,請先閱讀 [了解 Same-Origin Policy 以及 CORS 📝](https://github.com/twtrubiks/CORS-tutorial)。 634 | 635 | 設定是 django + nginx + uwsgi,這時候我們先來思考一個問題, 636 | 637 | 這樣你可能會問我,CORS 我是要設定在 Nginx 上,還是 Django,又或是兩邊都設定呢 :question: 638 | 639 | 秉持實驗的精神,三種況狀都來嘗試 ( 建議看影片 ): 640 | 641 | 方法一 : 兩邊都設定 ( 失敗 ) 642 | 643 | 如果你兩邊都設定,會出現類似以下的錯誤訊息 644 | 645 | ```text 646 | 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. 647 | ``` 648 | 649 | 方法二 : 只設定在 Nginx 上 ( 失敗 ) 650 | 651 | nginx 上設定 CORS,方法可參考 [here](https://github.com/twtrubiks/docker-django-nginx-uwsgi-postgres-tutorial/blob/master/nginx/my_nginx.conf#L37), 652 | 653 | 補充說明, 654 | 655 | **Access-Control-Allow-Origin** 656 | 657 | 允許的 domain,詳細說明可參考 [Access-Control-Allow-Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin)。 658 | 659 | **Access-Control-Allow-Credentials** 660 | 661 | 詳細說明可參考 [Access-Control-Allow-Credentials](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials)。 662 | 663 | **Access-Control-Allow-Methods** 664 | 665 | 詳細說明可參考 [Access-Control-Allow-Methods](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Allow-Methods)。 666 | 667 | **Access-Control-Allow-Headers** 668 | 669 | preflight request 指的是 CORS 發出的 OPTIONS request, 670 | 671 | ( 如果不知道什麼是 preflight request,可參考 [預檢請求](https://github.com/twtrubiks/CORS-tutorial#%E9%A0%90%E6%AA%A2%E8%AB%8B%E6%B1%82-preflight-request) ) 672 | 673 | actual request 指的是實際發出的 request,這邊是指 actual request。 674 | 675 | 詳細說明可參考 [Access-Control-Allow-Headers](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Allow-Headers)。 676 | 677 | **Access-Control-Expose-Headers** 678 | 679 | 指 broswer 可以使用/讀取那些 response 中的 headers,預設有這些 headers, 680 | 681 | `Cache-Control` `Content-Language` `Content-Type` `Expires` `Last-Modified` `Pragma`, 682 | 683 | 如果想拿到其他的 headers,就必須再手動加進去。 684 | 685 | 詳細說明可參考 [Access-Control-Expose-Headers](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Expose-Headers)。 686 | 687 | **Access-Control-Max-Age** 688 | 689 | preflight request 可以被 Cache 多長的時間。在時間內,broswer 會使用 Cache。 690 | 691 | 詳細說明可參考 [Access-Control-Max-Age](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Max-Age)。 692 | 693 | 如果你設定在 Nginx 上,你會發現你的 CORS headers 被吃掉了, 694 | 695 | ```text 696 | 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. 697 | ``` 698 | 699 | 這個問題我暫時也找不到方法,相關 [issuse](https://github.com/unbit/uwsgi/issues/1550)。 700 | 701 | ( 如果有人找到方法,請和小弟說一下,我去嘗試看看 ) 702 | 703 | 方法三 : 只設定在 Django 上 ( 成功 ) 704 | 705 | django 上可以設定 CORS,透過 django-cors-headers 方法可參考 [文章](https://github.com/twtrubiks/CORS-tutorial#cors)。 706 | 707 | 708 | **所以,如果你的環境是 django + nginx + uwsgi,CORS 建議使用 django-cors-headers 設定在 Django 上。** 709 | 710 | ## 透過 Nginx Log 分析 PV UV 711 | 712 | * [Youtube Tutorial - 透過 Nginx Log 分析 PV UV](https://youtu.be/mUyDVVX6OD4) 713 | 714 | 使用的可參考 [nginx.conf](https://github.com/twtrubiks/docker-django-nginx-uwsgi-postgres-tutorial/blob/master/nginx/nginx.conf), 715 | 716 | ```conf 717 | http { 718 | ...... 719 | log_format main '$host $remote_addr - $remote_user [$time_local] ' 720 | '"$request" $status $body_bytes_sent ' 721 | '"$http_referer" "$http_user_agent" ' 722 | '$request_time'; 723 | ...... 724 | ``` 725 | 726 | 這邊我也放了範例的 log 給各位, 可參考 [nginx-access.example_log](https://github.com/twtrubiks/docker-django-nginx-uwsgi-postgres-tutorial/blob/master/nginx/nginx-access.example_log) 727 | 728 | ```log 729 | 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 730 | 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 731 | 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 732 | 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 733 | ``` 734 | 735 | ### PV 736 | 737 | Page View 的縮寫, 可以簡單看成是一個 request 就是一個 PV. 738 | 739 | 計算所有的 PV 數 740 | 741 | ```cmd 742 | cat nginx-access.example_log | wc -l 743 | ``` 744 | 745 | 計算某天的 PV 數 746 | 747 | ```cmd 748 | cat nginx-access.example_log | sed -n '/05\/Apr\/2022/p' | wc -l 749 | ``` 750 | 751 | 計算某一個時間的 PV 數 (4-5) 752 | 753 | ```cmd 754 | cat nginx-access.example_log | sed -n '/06\/Apr\/2022:04/,/06\/Apr\/2022:05/p' | wc -l 755 | ``` 756 | 757 | 計算每秒的 PV 數 758 | 759 | ```cmd 760 | awk '{print $5}' nginx-access.example_log | cut -c 11-18 | sort | uniq -c | sort -n -r| head -n 10 761 | ``` 762 | 763 | 計算每分鐘的 PV 數 764 | 765 | ```cmd 766 | awk '{print $5}' nginx-access.example_log | cut -c 11-15 | sort | uniq -c | sort -n -r| head -n 10 767 | ``` 768 | 769 | 計算每小時的 PV 數 770 | 771 | ```cmd 772 | awk '{print $5}' nginx-access.example_log | cut -c 11-12 | sort | uniq -c | sort -n -r| head -n 10 773 | ``` 774 | 775 | `sort` 必需執行主要是因為 `uniq` 的關係, 可參考 [uniq](https://github.com/twtrubiks/linux-note#uniq). 776 | 777 | ### UV 778 | 779 | Unique Visitor 的縮寫, 獨立的訪客, 每個訪客一天只算一次. 780 | 781 | 這邊簡單用 IP 來當作獨立的訪客, 782 | 783 | 依據 IP 計算 UV 數量 784 | 785 | ```cmd 786 | ❯ awk '{print $2}' nginx-access.example_log | sort | uniq -c | wc -l 787 | 2 788 | ``` 789 | 790 | ### IP 791 | 792 | 計算每個 IP 出現次數 793 | 794 | ```cmd 795 | ❯ awk '{print $2}' nginx-access.example_log | sort | uniq -c | sort -n 796 | 2 172.30.0.1 797 | 2 172.30.0.2 798 | ``` 799 | 800 | 計算訪問最頻繁的前 10 個 IP 801 | 802 | ```cmd 803 | awk '{print $2}' nginx-access.example_log | sort -n | uniq -c | sort -n -r | head -n 10 804 | ``` 805 | 806 | 查詢某 IP 的訪問 URL 狀態 807 | 808 | ```cmd 809 | grep '172.30.0.2' nginx-access.example_log | awk '{print $8}' | sort | uniq -c | sort -n -r 810 | ``` 811 | 812 | ### 其他 813 | 814 | 查詢訪問最頻繁的 URL 815 | 816 | ```cmd 817 | awk '{print $8}' nginx-access.example_log | sort | uniq -c | sort -n 818 | ``` 819 | 820 | 查詢訪問最頻繁的 URL ( 排除特定 URL ) 821 | 822 | ```cmd 823 | grep -v "/api/" nginx-access.example_log | awk '{print $8}' | sort | uniq -c | sort -n -r 824 | ``` 825 | 826 | 查詢傳輸時間超過 0.3 秒的頁面 ( 記得要在 [nginx.conf](https://github.com/twtrubiks/docker-django-nginx-uwsgi-postgres-tutorial/blob/master/nginx/nginx.conf) 加入 `$request_time` ) 827 | 828 | ```cmd 829 | ❯ cat nginx-access.example_log | awk '($NF > 0.3){print $21}' | sort -n | uniq -c | sort -n -r 830 | 1 0.506 831 | 1 0.306 832 | ``` 833 | 834 | 查詢訪問最頻訪的 host 835 | 836 | ```cmd 837 | ❯ awk '{print $1}' nginx-access.example_log | sort | uniq -c | sort -n 838 | 4 localhost 839 | ``` 840 | 841 | 如果以上指令不熟, 可參考 [紀錄一些 linux 的指令](https://github.com/twtrubiks/linux-note). 842 | 843 | ## 封鎖惡意蜘蛛爬蟲 844 | 845 | 有些蜘蛛爬蟲真的品質很差, 甚至影響系統的速度, 這邊教大家如何阻擋, 846 | 847 | 到你的 nginx 設定中加入 (這邊阻擋 MJ12bot) 848 | 849 | ```conf 850 | if ($http_user_agent ~* (MJ12bot) ) { 851 | return 444; 852 | } 853 | ``` 854 | 855 | `~*` 代表不區分大小寫 856 | 857 | `~` 代表區分大小寫 858 | 859 | 如果想要 ban 很多爬蟲, 860 | 861 | ```conf 862 | if ($http_user_agent ~* (MJ12bot|Semrush|DataForSeo|Yandex|Ahrefs|Petal|Dot)){ 863 | return 444; 864 | } 865 | ``` 866 | 867 | ( http response code 也有人使用 410, 但似乎更多人使用 444) 868 | 869 | 重啟 nginx 後, 使用以下指令檢查是否成功 870 | 871 | ```cmd 872 | ❯ curl -I -A 'mj12bot' YOUR_DOMAIN 873 | curl: (52) Empty reply from server 874 | ``` 875 | 876 | 其他範例 877 | 878 | ```cmd 879 | curl -A "Mozilla/5.0 (compatible; SemrushBot/6~bl; +http://www.semrush.com/bot.html)" YOUR_DOMAIN 880 | 881 | curl -A "Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://ahrefs.com/robot/)" YOUR_DOMAIN 882 | 883 | 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 884 | ``` 885 | 886 | `-I`, `--head` Show document info only. 887 | 888 | `-A`, `--user-agent ` Send User-Agent to server. 889 | 890 | 如果成功阻擋, 會和上面顯示一樣. 891 | 892 | 如果沒有成功阻擋, 會顯示正常的 200. 893 | 894 | 也可以參考 [here](https://gist.github.com/hans2103/733b8eef30e89c759335017863bd721d) 895 | 896 | ## 後記: 897 | 898 | 自己也是第一次建立 Django + Nginx + uWSGI + Postgres ,中間也搞了超久 :scream:,但我真心推薦 Docker, 899 | 900 | 用 Docker 玩這個真的很爽,玩壞了,就直接刪掉重來就好,神速快,而且透過這個練習,大家應該還會 901 | 902 | 看到 Nginx 其實有非常多的功能可以去把玩,像是 Load balance 之類的,可以更瞭解 Server,我也是透過 903 | 904 | 他才瞭解自己不懂的地方還是很多,總之,建議大家動手下去依照我的步驟玩玩看,相信多多少少會有收穫 905 | 906 | ,我也是 Docker 新手,如果我有任何講錯的地方,請麻煩大家和我說,我會再修改 :blush: 907 | 908 | 如果意猶未盡,延伸閱讀 :satisfied: 909 | 910 | * [docker-letsencrypt-django-nginx-proxy-uwsgi-postgres](https://github.com/twtrubiks/docker-letsencrypt-django-nginx-proxy-uwsgi-postgres) 911 | 912 | * [實戰 Docker + Django + Nginx + uWSGI + Postgres - Load Balance 📝](https://github.com/twtrubiks/docker-django-nginx-uwsgi-postgres-load-balance-tutorial) 913 | * [Docker Swarm 基本教學 - 從無到有 Docker-Swarm-Beginners-Guide📝](https://github.com/twtrubiks/docker-swarm-tutorial) 914 | 915 | ## 執行環境 916 | 917 | * Linux 918 | * Mac 919 | * Python 3.8.2 920 | * windows 10 921 | 922 | ## Reference 923 | 924 | * [https://docs.docker.com/](https://docs.docker.com/) 925 | * [uwsgi-docs](https://uwsgi-docs.readthedocs.io/en/latest/tutorials/Django_and_nginx.html) 926 | 927 | ## Donation 928 | 929 | 文章都是我自己研究內化後原創,如果有幫助到您,也想鼓勵我的話,歡迎請我喝一杯咖啡:laughing: 930 | 931 | ![alt tag](https://i.imgur.com/LRct9xa.png) 932 | 933 | [贊助者付款](https://payment.opay.tw/Broadcaster/Donate/9E47FDEF85ABE383A0F5FC6A218606F8) 934 | 935 | ## License 936 | 937 | MIT license 938 | -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8.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 9 | 10 | # for entry point 11 | RUN chmod +x /docker_api/docker-entrypoint.sh -------------------------------------------------------------------------------- /api/django_rest_framework_tutorial/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twtrubiks/docker-django-nginx-uwsgi-postgres-tutorial/d9c546172741bc074ffc2f54efa2a8d30b8d26e1/api/django_rest_framework_tutorial/__init__.py -------------------------------------------------------------------------------- /api/django_rest_framework_tutorial/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for django_rest_framework_tutorial project. 3 | It exposes the ASGI callable as a module-level variable named ``application``. 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ 6 | """ 7 | 8 | import os 9 | 10 | from django.core.asgi import get_asgi_application 11 | 12 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_rest_framework_tutorial.settings') 13 | 14 | application = get_asgi_application() -------------------------------------------------------------------------------- /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 3.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 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 | CORS_ORIGIN_ALLOW_ALL = True # corsheaders 31 | 32 | # CORS_ORIGIN_WHITELIST = ( 33 | # # 'google.com', 34 | # # 'hostname.example.com', 35 | # '127.0.0.1:8002', 36 | # ) 37 | 38 | CORS_ALLOW_METHODS = [ 39 | # 'DELETE', 40 | 'GET', 41 | 'OPTIONS', 42 | 'PATCH', 43 | 'POST', 44 | 'PUT', 45 | ] 46 | 47 | 48 | CORS_ALLOW_HEADERS = [ 49 | 'accept', 50 | 'accept-encoding', 51 | 'authorization', 52 | 'content-type', 53 | 'dnt', 54 | 'origin', 55 | 'user-agent', 56 | 'x-csrftoken', 57 | 'x-requested-with', 58 | 'Cache-Control' 59 | ] 60 | 61 | 62 | # Application definition 63 | 64 | INSTALLED_APPS = [ 65 | 'musics.apps.MusicsConfig', 66 | 'django.contrib.admin', 67 | 'django.contrib.auth', 68 | 'django.contrib.contenttypes', 69 | 'django.contrib.sessions', 70 | 'django.contrib.messages', 71 | 'django.contrib.staticfiles', 72 | 'corsheaders', # django-cors-headers 73 | 'rest_framework', 74 | ] 75 | 76 | MIDDLEWARE = [ 77 | 'django.middleware.security.SecurityMiddleware', 78 | 'django.contrib.sessions.middleware.SessionMiddleware', 79 | 'corsheaders.middleware.CorsMiddleware', # CORS 80 | 'django.middleware.common.CommonMiddleware', 81 | 'django.middleware.csrf.CsrfViewMiddleware', 82 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 83 | 'django.contrib.messages.middleware.MessageMiddleware', 84 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 85 | ] 86 | 87 | ROOT_URLCONF = 'django_rest_framework_tutorial.urls' 88 | 89 | TEMPLATES = [ 90 | { 91 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 92 | 'DIRS': [], 93 | 'APP_DIRS': True, 94 | 'OPTIONS': { 95 | 'context_processors': [ 96 | 'django.template.context_processors.debug', 97 | 'django.template.context_processors.request', 98 | 'django.contrib.auth.context_processors.auth', 99 | 'django.contrib.messages.context_processors.messages', 100 | ], 101 | }, 102 | }, 103 | ] 104 | 105 | WSGI_APPLICATION = 'django_rest_framework_tutorial.wsgi.application' 106 | 107 | 108 | # Database 109 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 110 | 111 | # DATABASES = { 112 | # 'default': { 113 | # 'ENGINE': 'django.db.backends.sqlite3', 114 | # 'NAME': BASE_DIR / 'db.sqlite3', 115 | # } 116 | # } 117 | 118 | DATABASES = { 119 | 'default': { 120 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 121 | 'NAME': 'postgres', 122 | 'USER': 'postgres', 123 | 'PASSWORD': 'password123', 124 | 'HOST': 'db', 125 | 'PORT': 5432, 126 | } 127 | } 128 | 129 | 130 | # Password validation 131 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 132 | 133 | AUTH_PASSWORD_VALIDATORS = [ 134 | { 135 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 136 | }, 137 | { 138 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 139 | }, 140 | { 141 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 142 | }, 143 | { 144 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 145 | }, 146 | ] 147 | 148 | 149 | # Internationalization 150 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 151 | 152 | LANGUAGE_CODE = 'en-us' 153 | 154 | TIME_ZONE = 'Asia/Taipei' 155 | 156 | USE_I18N = True 157 | 158 | USE_L10N = True 159 | 160 | USE_TZ = True 161 | 162 | 163 | # Static files (CSS, JavaScript, Images) 164 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 165 | 166 | STATIC_URL = '/static/' 167 | 168 | # Default primary key field type 169 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 170 | 171 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 172 | STATIC_ROOT = Path('/').joinpath(BASE_DIR, 'static/') -------------------------------------------------------------------------------- /api/django_rest_framework_tutorial/urls.py: -------------------------------------------------------------------------------- 1 | """django_rest_framework_tutorial URL Configuration 2 | The `urlpatterns` list routes URLs to views. For more information please see: 3 | https://docs.djangoproject.com/en/3.2/topics/http/urls/ 4 | Examples: 5 | Function views 6 | 1. Add an import: from my_app import views 7 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 8 | Class-based views 9 | 1. Add an import: from other_app.views import Home 10 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 11 | Including another URLconf 12 | 1. Import the include() function: from django.urls import include, path 13 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 14 | """ 15 | from django.contrib import admin 16 | from django.urls import include, path 17 | from rest_framework.routers import DefaultRouter 18 | from musics.views import MusicViewSet 19 | 20 | router = DefaultRouter() 21 | router.register('musics', MusicViewSet) 22 | 23 | 24 | urlpatterns = [ 25 | # path('admin/', admin.site.urls), 26 | # for rest_framework 27 | path('api/', include(router.urls)), 28 | # for rest_framework auth 29 | path('api-auth/', include('rest_framework.urls', namespace='rest_framework')) 30 | ] -------------------------------------------------------------------------------- /api/django_rest_framework_tutorial/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_rest_framework_tutorial project. 3 | It exposes the WSGI callable as a module-level variable named ``application``. 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ 6 | """ 7 | 8 | import os 9 | 10 | from django.core.wsgi import get_wsgi_application 11 | 12 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_rest_framework_tutorial.settings') 13 | 14 | application = get_wsgi_application() -------------------------------------------------------------------------------- /api/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo 'Run migration' 3 | python3 manage.py makemigrations musics 4 | python3 manage.py migrate 5 | echo 'Create Super User' 6 | python3 manage.py createsuperuser --noinput || echo "Super user already created" 7 | echo 'Collect Static' 8 | python3 manage.py collectstatic --noinput 9 | exec "$@" -------------------------------------------------------------------------------- /api/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_rest_framework_tutorial.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() -------------------------------------------------------------------------------- /api/musics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twtrubiks/docker-django-nginx-uwsgi-postgres-tutorial/d9c546172741bc074ffc2f54efa2a8d30b8d26e1/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 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'musics' 7 | -------------------------------------------------------------------------------- /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 | from django.test import TestCase 2 | 3 | # Create your tests here. -------------------------------------------------------------------------------- /api/musics/views.py: -------------------------------------------------------------------------------- 1 | from .models import Music 2 | from .serializers import MusicSerializer 3 | from rest_framework import viewsets, status 4 | from rest_framework.decorators import action 5 | from django.shortcuts import get_object_or_404 6 | from rest_framework.response import Response 7 | from rest_framework.permissions import IsAuthenticated 8 | 9 | class MusicViewSet(viewsets.ModelViewSet): 10 | queryset = Music.objects.all() 11 | serializer_class = MusicSerializer 12 | permission_classes = (IsAuthenticated,) 13 | # parser_classes = (JSONParser,) 14 | 15 | # [GET] /api/musics/{pk}/detail/ 16 | @action(detail=True, methods=['get'], url_path='detail') 17 | def detail_action(self, request, pk=None): 18 | music = get_object_or_404(Music, pk=pk) 19 | result = { 20 | 'singer': music.singer, 21 | 'song': music.song 22 | } 23 | return Response(result, status=status.HTTP_200_OK) 24 | 25 | # [GET] /api/musics/all_singer/ 26 | @action(detail=False, methods=['get'], url_path='all_singer') 27 | def all_singer(self, request): 28 | music = Music.objects.values_list('singer', flat=True) -------------------------------------------------------------------------------- /api/requirements.txt: -------------------------------------------------------------------------------- 1 | Django==3.2.8 2 | djangorestframework==3.12.4 3 | psycopg2==2.9.1 4 | 5 | uwsgi 6 | django-cors-headers 7 | -------------------------------------------------------------------------------- /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; -------------------------------------------------------------------------------- /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 | - "80:80" 22 | volumes: 23 | - api_data:/docker_api 24 | - ./log:/var/log/nginx 25 | - ./my_htpasswd:/my_htpasswd 26 | depends_on: 27 | - api 28 | 29 | api: 30 | container_name: api-container 31 | build: ./api 32 | restart: always 33 | entrypoint: /docker_api/docker-entrypoint.sh 34 | # command: uwsgi --emperor uwsgi.ini 35 | command: uwsgi --ini uwsgi.ini 36 | 37 | # ref. 38 | # https://docs.djangoproject.com/en/3.0/ref/django-admin/#django-admin-createsuperuser 39 | environment: 40 | - DJANGO_SUPERUSER_USERNAME=admin 41 | - DJANGO_SUPERUSER_PASSWORD=admin1234 42 | - DJANGO_SUPERUSER_EMAIL=admin@twtrubiks.com 43 | 44 | # ports: 45 | # - "8002:8000" 46 | volumes: 47 | - api_data:/docker_api 48 | depends_on: 49 | - db 50 | 51 | # auto migrate django 52 | # migration: 53 | # build: ./api 54 | # command: 55 | # - /bin/sh 56 | # - -c 57 | # - | 58 | # python manage.py collectstatic --noinput 59 | # python manage.py migrate 60 | # python manage.py makemigrations musics 61 | # python manage.py migrate 62 | # volumes: 63 | # - api_data:/docker_api 64 | # depends_on: 65 | # - db 66 | 67 | volumes: 68 | api_data: 69 | pgdata: 70 | 71 | -------------------------------------------------------------------------------- /my_htpasswd/htpasswd: -------------------------------------------------------------------------------- 1 | user1:8uxCGNPhjFqiw:123 -------------------------------------------------------------------------------- /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; # for a file socket 6 | } 7 | 8 | # configuration of the server 9 | server { 10 | # the port your site will be served on 11 | listen 80; 12 | # index index.html; 13 | # the domain name it will serve for 14 | # substitute your machine's IP address or FQDN 15 | server_name twtrubiks.com www.twtrubiks.com; 16 | charset utf-8; 17 | 18 | client_max_body_size 75M; # adjust to taste 19 | 20 | # Django media 21 | # location /media { 22 | # alias /docker_api/static/media; # your Django project's media files - amend as required 23 | # } 24 | 25 | location /static { 26 | alias /docker_api/static; # your Django project's static files - amend as required 27 | } 28 | 29 | location /nginx/status { 30 | # 啟用 stub_status 31 | stub_status on; 32 | 33 | # 關閉/啟用 log 34 | # access_log /usr/local/nginx/logs/status.log; 35 | access_log off; 36 | 37 | auth_basic "NginxStatus"; 38 | auth_basic_user_file /my_htpasswd/htpasswd; 39 | 40 | # 限制可存取的 IP 41 | # allow 127.0.0.1; 42 | # deny all; 43 | } 44 | 45 | location / { 46 | # 限制可存取的 IP 47 | # allow 127.0.0.1; 48 | # deny all; 49 | 50 | uwsgi_pass uwsgi; 51 | 52 | # nginx CORS 53 | 54 | # nginx+uwssgi issuse 55 | # https://github.com/unbit/uwsgi/issues/1550 56 | 57 | # add_header 'Access-Control-Allow-Origin' '*'; 58 | # add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; 59 | # add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'; 60 | # add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range'; 61 | 62 | include /etc/nginx/uwsgi_params; # the uwsgi_params file you installed 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /nginx/nginx-access.example_log: -------------------------------------------------------------------------------- 1 | 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 2 | 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 3 | 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 4 | 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 5 | -------------------------------------------------------------------------------- /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 | log_format main '$host $remote_addr - $remote_user [$time_local] ' 20 | '"$request" $status $body_bytes_sent ' 21 | '"$http_referer" "$http_user_agent" ' 22 | '$request_time'; 23 | 24 | 25 | access_log /var/log/nginx/access.log main; 26 | 27 | sendfile on; 28 | #tcp_nopush on; 29 | 30 | keepalive_timeout 65; 31 | 32 | # Gzip Compression 33 | gzip on; 34 | # gzip_min_length 1000; 35 | gzip_types text/plain application/xml; 36 | gzip_proxied expired no-cache no-store private auth; 37 | gzip_vary on; 38 | 39 | # include /etc/nginx/conf.d/*.conf; 40 | include /etc/nginx/sites-available/*; 41 | } -------------------------------------------------------------------------------- /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 | } --------------------------------------------------------------------------------