1
1
[[chapter_10_production_readiness]]
2
- == Making our App Production-Ready
2
+ == Making Our App Production-Ready
3
3
4
4
.A Note for Early Release Readers
5
5
****
@@ -47,10 +47,13 @@ Instead, we'll use the popular Gunicorn Python/WSGI server.
47
47
In addition, several options in _settings.py_ are currently unacceptable.
48
48
`DEBUG=True`, is strongly recommended against for production,
49
49
we'll want to set a unique `SECRET_KEY`,
50
- and as we'll see, other things will come up.
50
+ and, as we'll see, other things will come up.
51
51
52
52
Let's go through and see if we can fix things one by one.
53
53
54
+ // SEBASTIAN: How about linking to Django's documentation
55
+ // https://docs.djangoproject.com/en/5.0/howto/deployment/
56
+ // somewhere later in the chapter for curious readers?
54
57
55
58
=== Switching to Gunicorn
56
59
@@ -88,6 +91,8 @@ CMD gunicorn --bind :8888 superlists.wsgi:application <2>
88
91
which is usually a function called `application`.
89
92
Django provides one in _superlists/wsgi.py_.
90
93
94
+ // TODO; this is a good place to switch to using array syntax for CMD maybe?
95
+
91
96
As in the previous chapter, we can use the `docker build && docker run`
92
97
pattern to try out our changes by rebuilding and rerunning our container:
93
98
@@ -99,6 +104,12 @@ $ *docker build -t superlists . && docker run \
99
104
-it superlists*
100
105
----
101
106
107
+ // DAVID: Incidentally I got the following error:
108
+ // Bind for 0.0.0.0:8888 failed: port is already allocated.
109
+ // Turned out the previous container was still running,
110
+ // I just used the docker kill process you taught me about earlier.
111
+ // Not sure if it's worth including that here, possibly clutter?
112
+
102
113
103
114
==== The FTs catch a problem with static files
104
115
@@ -187,6 +198,14 @@ $ *docker build -t superlists . && docker run \
187
198
And if you take another manual look at your site, things should look much healthier.
188
199
Let's rerun our FTs to confirm:
189
200
201
+ // DAVID: Incidentally as per your suggestion Harry I have just been skipping
202
+ // the requirements.txt and instead just amending the pip install in the Dockerfile.
203
+ // If we do that, however, we'll need to make sure readers rebuild the image each time
204
+ // we add a requirement - such as at this point.
205
+
206
+
207
+ // JAN: That doesn't work until you install whitenoise to venv on local machine
208
+
190
209
[role="small-code"]
191
210
[subs="specialcharacters,macros"]
192
211
----
@@ -201,7 +220,13 @@ Ran 3 tests in 10.718s
201
220
OK
202
221
----
203
222
204
- Phew. Let's commit that
223
+ // DAVID: I got
224
+ // UserWarning: No directory at: /Users/david.seddon/Documents/reviewing/goat-book/src/static/
225
+ // mw_instance = middleware(adapted_handler)
226
+ // I think we need to move the static folder into src too, in the previous chapter.
227
+
228
+
229
+ Phew. Let's commit that:
205
230
206
231
[subs="specialcharacters,quotes"]
207
232
----
@@ -229,6 +254,8 @@ I'm sure you'll come across many others.]
229
254
230
255
The `pip freeze` command will show us everything that's installed in our virtualenv at the moment:
231
256
257
+ // JAN: This is true only if one installs the libraries locally. So far when following the book we installed them only inside Docker image
258
+
232
259
[subs="specialcharacters,quotes"]
233
260
----
234
261
$ *pip freeze*
@@ -285,11 +312,18 @@ whitenoise==6.6.0
285
312
====
286
313
287
314
315
+ That's a good first cut, let's commit it:
316
+
317
+ // SEBASTIAN I am not very fond of freezing just "top-level" dependencies of the project,
318
+ // . This is not guaranteeing the reproducible builds and I failed to build & deploy because of that more than once.
319
+ // But I understand it's a trade-off so the reader does get bogged down.
320
+ // TODO: add a note about this and a link to some more reading
321
+
288
322
----
289
- # that's a good first cut, let's commit it:
290
323
$ *git add requirements.txt*
291
324
$ *git commit -m "Add a requirements.txt with Django, gunicorn and whitenoise"*
292
325
----
326
+
293
327
You may be wondering why we didn't add our other dependency,
294
328
Selenium, to our requirements,
295
329
or why we didn't just add _all_ the dependencies,
@@ -349,6 +383,15 @@ TIP: Forgetting the `-r` and running `pip install requirements.txt`
349
383
(which is thankfully much more helpful than it used to be).
350
384
It's a mistake I still make, _all the time_.
351
385
386
+
387
+ // CSANAD: It's fine for now, but I would definitely put the requirements under
388
+ // /tmp and then `rm` it after `pip install`. Also, using a non-privileged
389
+ // user is important, something like:
390
+ // `adduser --no-create-home --disabled-password todoapp`
391
+ // and then setting the user in the Dockerfile with `USER todoapp`.
392
+ // But we can cover this in a later chapter (the next one looks like a good fit,
393
+ // since it's related to the app being production ready).
394
+
352
395
Let's do a build & run & test to check everything still works:
353
396
354
397
[subs="specialcharacters,quotes"]
@@ -387,12 +430,13 @@ See http://www.clearlytech.com/2014/01/04/12-factor-apps-plain-english/[
387
430
Another common way of handling this
388
431
is to have different versions of _settings.py_ for dev and prod.
389
432
That can work fine too, but it can get confusing to manage.
390
- Environment variables also have the advantage of working for non-Django stuff too...
433
+ Environment variables also have the advantage of working for non-Django stuff too.
391
434
]
392
435
393
436
394
437
==== Setting DEBUG=True and SECRET_KEY
395
438
439
+ //RITA: What do you mean by "this"? Please add a word or two for context.
396
440
There's lots of ways you might do this.
397
441
Here's what I propose; it may seem a little fiddly,
398
442
but I'll provide a little justification for each choice.
@@ -437,7 +481,7 @@ TIP: Better to fail hard than allow a typo in an environment variable name to
437
481
438
482
==== Setting environment variables inside the Dockerfile
439
483
440
- Now let's set that environment variable in our Dockerfile using then `ENV` directive:
484
+ Now let's set that environment variable in our Dockerfile using the `ENV` directive:
441
485
442
486
[role="sourcecode"]
443
487
.Dockerfile (ch10l007)
@@ -470,7 +514,7 @@ $ pass:specialcharacters,quotes[*docker build -t superlists . && docker run \
470
514
KeyError: 'DJANGO_SECRET_KEY'
471
515
----
472
516
473
- Ooops, and I forgot to set said secret key env var,
517
+ Oops. I forgot to set said secret key env var,
474
518
mere seconds after having dreamt it up!
475
519
476
520
@@ -505,7 +549,7 @@ AssertionError: 'To-Do' not found in 'Bad Request (400)'
505
549
506
550
==== ALLOWED_HOSTS is Required When Debug Mode is Turned Off
507
551
508
- Not quite! Let's take a look manually: <<django-400-error>>.
552
+ It's not quite working yet ! Let's take a look manually: <<django-400-error>>.
509
553
510
554
[[django-400-error]]
511
555
.An ugly 400 error
@@ -534,7 +578,7 @@ image::images/search-results-400-bad-request.png["Duckduckgo search results with
534
578
`ALLOWED_HOSTS` is a security setting
535
579
designed to reject requests that are likely to be forged, broken or malicious
536
580
because they don't appear to be asking for your site
537
- (HTTP request contain the address they were intended for in a header called "Host").
581
+ (HTTP requests contain the address they were intended for in a header called "Host").
538
582
539
583
By default, when DEBUG=True, `ALLOWED_HOSTS` effectively allows _localhost_,
540
584
our own machine, so that's why it was working OK until now.
@@ -563,7 +607,7 @@ else:
563
607
====
564
608
565
609
This is a setting that we want to change,
566
- depending on whether our docker image is running locally,
610
+ depending on whether our Docker image is running locally,
567
611
or on a server, so we'll use the `-e` flag again:
568
612
569
613
@@ -581,7 +625,7 @@ $ *docker build -t superlists . && docker run \
581
625
==== Collectstatic is Required when Debug is Turned Off
582
626
583
627
An FT run (or just looking at the site) reveals that we've had a regression
584
- in our static files.
628
+ in our static files:
585
629
586
630
[role="small-code"]
587
631
[subs="specialcharacters,macros"]
@@ -615,6 +659,13 @@ CMD gunicorn --bind :8888 superlists.wsgi:application
615
659
----
616
660
====
617
661
662
+ // DAVID: It would be nice to explain the difference between RUN and CMD.
663
+
664
+ // DAVID: Interestingly when I did this I put the RUN directive after the ENV
665
+ // directive, which led to a KeyError: 'DJANGO_SECRET_KEY' which foxed me for a bit.
666
+ // Might be worth calling out that we're running collectstatic in debug mode.
667
+
668
+
618
669
// TODO: gitignore src/static
619
670
620
671
Well, it was fiddly, but that should get us to passing tests!
631
682
632
683
We're nearly ready to ship to production!
633
684
685
+ // DAVID: It might be worth pointing out what Whitenoise is actually doing.
686
+ // From what I understand, we're still using Django to serve static files.
687
+
634
688
Let's quickly adjust our gitignore, since the static folder is in a new place:
635
689
636
690
//0010
@@ -645,6 +699,21 @@ $ *git commit -am "Add collectstatic to dockerfile, and new location to gitignor
645
699
----
646
700
647
701
702
+ === Switching to a nonroot user
703
+
704
+ TODO: WIP, this is definitely a good idea for security, needs writing up.
705
+
706
+ Dockerfile should gain some lines a bit like this:
707
+
708
+ .Dockerfile (ch10l0XX)
709
+ ====
710
+ [source,dockerfile]
711
+ ----
712
+ RUN addgroup --system nonroot && adduser --system --group nonroot
713
+
714
+ USER nonroot
715
+ ----
716
+
648
717
649
718
650
719
=== Configuring logging
@@ -666,7 +735,6 @@ $ *rm src/db.sqlite3*
666
735
$ *touch src/db.sqlite3* # otherwise the --mount type=bind will complain
667
736
----
668
737
669
-
670
738
Now if you run the tests, you'll see they fail;
671
739
672
740
[role="small-code"]
@@ -680,6 +748,10 @@ selenium.common.exceptions.NoSuchElementException: Message: Unable to locate
680
748
element: [id="id_list_table"]; [...]
681
749
----
682
750
751
+ // DAVID: Got me thinking, I'm not always clear when I need to rebuild the image.
752
+ // I would have thought I might need to do it here, but I didn't. Might be worth
753
+ // explaining in the previous chapter when we do.
754
+
683
755
And you might spot in the browser that we just see a minimal error page,
684
756
with no debug info (try it manually if you like):
685
757
@@ -715,7 +787,7 @@ I mean, yes, separating the concepts of handlers and loggers and filters,
715
787
and making it all configurable in a nested hierarchy is all well and good
716
788
and covers every possible use case,
717
789
but sometimes you just wanna say "just print stuff to stdout pls",
718
- and you wish that configuring the simplest thing was a little easier].
790
+ and you wish that configuring the simplest thing was a little easier. ].
719
791
720
792
Here's pretty much the simplest possible logging config
721
793
which just prints everything to the console (ie standard out).
@@ -743,7 +815,6 @@ Rebuild and restart our container,
743
815
try the FT again (or submitting a new list item manually)
744
816
and we now should see a clear error message:
745
817
746
-
747
818
----
748
819
Internal Server Error: /lists/new
749
820
Traceback (most recent call last):
@@ -759,7 +830,7 @@ Traceback (most recent call last):
759
830
django.db.utils.OperationalError: no such table: lists_list
760
831
----
761
832
762
- Re-create the datase with `./src/manage.py migrate` and we'll be back to a working state.
833
+ Re-create the database with `./src/manage.py migrate` and we'll be back to a working state.
763
834
764
835
Don't forget to commit our changes to _settings.py_,
765
836
and I think we can call it job done!
@@ -768,6 +839,8 @@ when considering production-readiness,
768
839
we've worked in small steps and used our tests all the way along,
769
840
and we're now ready to deploy our container to a real server!
770
841
842
+ Find out how, in our next exciting installment...
843
+
771
844
772
845
[role="pagebreak-before less_space"]
773
846
.Production-Readiness Config
0 commit comments