<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kamil Marut</title>
    <description>The latest articles on DEV Community by Kamil Marut (@exler).</description>
    <link>https://dev.to/exler</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F741328%2F2a0759b1-552f-42e0-83c1-1fbfffd89ee9.jpg</url>
      <title>DEV Community: Kamil Marut</title>
      <link>https://dev.to/exler</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/exler"/>
    <language>en</language>
    <item>
      <title>ZBar and the Memory Usage Mystery</title>
      <dc:creator>Kamil Marut</dc:creator>
      <pubDate>Thu, 10 Jul 2025 11:30:00 +0000</pubDate>
      <link>https://dev.to/exler/zbar-and-the-memory-usage-mystery-ppk</link>
      <guid>https://dev.to/exler/zbar-and-the-memory-usage-mystery-ppk</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Last week, my team was forwarded this screenshot from Grafana, which showed a sudden spike in memory usage of our processing service:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhhmgc0aavn3q0rtkcmda.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhhmgc0aavn3q0rtkcmda.png" alt="Memory usage in Grafana" width="547" height="143"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We were asked to look into it, as the traces were unclear and the spike was causing stability issues in production. Fortunately, the logs accurately pointed us to the document involved in the spike.&lt;br&gt;
Unfortunately, it was just a single-page PDF document that weighed only around ~1MB, so at first glance, we couldn't draw any conclusions from it.&lt;/p&gt;

&lt;p&gt;One of my team members, started investigating the issue and quickly found that the spike was caused by the ZBar library, which we use to decode QR codes in our service.&lt;/p&gt;

&lt;p&gt;It was unclear to us at first why ZBar consumed so much memory, so I decided to get cracking.&lt;/p&gt;
&lt;h2&gt;
  
  
  Understanding the current processing
&lt;/h2&gt;

&lt;p&gt;So, first we need to understand what exactly is the state of the current QR code processing. Since ZBar does not support processing PDF documents directly, we first convert the PDF to an image using the &lt;code&gt;pdftoppm&lt;/code&gt; command line utility (part of the Poppler library). Then, we use ZBar to try and detect the QR code in the image. Easy and straightforward; and it worked well for us so far.&lt;/p&gt;
&lt;h2&gt;
  
  
  Preparing the test document
&lt;/h2&gt;

&lt;p&gt;As the original document that caused the spike is confidential, I downloaded a sample PDF invoice from the Internet, stuck a QR code on it, printed it out and scanned to a PDF (to at least try to replicate the process the customer used). Here's how the document looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fno5g37af1cw4rgs4hwc3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fno5g37af1cw4rgs4hwc3.jpg" alt="Test document with QR code" width="800" height="1141"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The actual PDF file that I created weighs 1.74MB, which is slightly larger than the original document that caused the spike.&lt;/p&gt;
&lt;h2&gt;
  
  
  What is this resolution?
&lt;/h2&gt;

&lt;p&gt;Let's look at the memory footprint of this processing with a simple benchmarking script using &lt;code&gt;tracemalloc&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;And this is the output of the &lt;a href="https://gist.github.com/exler/ea78f6adbaac0fdd1c886740e72c68f6" rel="noopener noreferrer"&gt;benchmarking script&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python ./zbar_process.py testdata.pdf 
Starting PDF processing &lt;span class="k"&gt;for &lt;/span&gt;testdata.pdf at 500 DPI...
PDF converted to 1 images.
Processing image 1/1
  - Found barcode: http://kamilmarut.com
  - Current memory usage: 0.78MB
  - Peak memory usage:   46.81MB
&lt;span class="nt"&gt;--------------------&lt;/span&gt;
Processing finished &lt;span class="k"&gt;in &lt;/span&gt;2.25 seconds.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wow! The peak memory is nearly 27 times the size of the original PDF document! We have successfully read out the QR code from the document, but at what cost? The memory footprint is quite high, so let's try to understand why.&lt;/p&gt;

&lt;p&gt;The first clue is the one that's already printed in the output: we have converted the PDF to an image at 500 DPI. That is most likely unnecessarily high and will definitely eat up a lot of memory!&lt;br&gt;
Since this is the value that was set on production, we should try to go down with it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python ./zbar_process.py CCI10072025.pdf &lt;span class="nt"&gt;--resolution&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;300
Starting PDF processing &lt;span class="k"&gt;for &lt;/span&gt;CCI10072025.pdf at 300 DPI...
PDF converted to 1 images.
Processing image 1/1
  - Found barcode: http://kamilmarut.com
  - Current memory usage: 0.78MB
  - Peak memory usage:   17.35MB
&lt;span class="nt"&gt;--------------------&lt;/span&gt;
Processing finished &lt;span class="k"&gt;in &lt;/span&gt;1.14 seconds.

&lt;span class="nv"&gt;$ &lt;/span&gt;python ./zbar_process.py CCI10072025.pdf &lt;span class="nt"&gt;--resolution&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100
Starting PDF processing &lt;span class="k"&gt;for &lt;/span&gt;CCI10072025.pdf at 100 DPI...
PDF converted to 1 images.
Processing image 1/1
  - Found barcode: http://kamilmarut.com
  - Current memory usage: 0.78MB
  - Peak memory usage:   2.62MB
&lt;span class="nt"&gt;--------------------&lt;/span&gt;
Processing finished &lt;span class="k"&gt;in &lt;/span&gt;0.41 seconds.

&lt;span class="nv"&gt;$ &lt;/span&gt;python ./zbar_process.py CCI10072025.pdf &lt;span class="nt"&gt;--resolution&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;80
Starting PDF processing &lt;span class="k"&gt;for &lt;/span&gt;CCI10072025.pdf at 80 DPI...
PDF converted to 1 images.
Processing image 1/1
  - Found barcode: http://kamilmarut.com
  - Current memory usage: 0.78MB
  - Peak memory usage:   1.96MB
&lt;span class="nt"&gt;--------------------&lt;/span&gt;
Processing finished &lt;span class="k"&gt;in &lt;/span&gt;0.38 seconds.

&lt;span class="nv"&gt;$ &lt;/span&gt;python ./zbar_process.py CCI10072025.pdf &lt;span class="nt"&gt;--resolution&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;50
Starting PDF processing &lt;span class="k"&gt;for &lt;/span&gt;CCI10072025.pdf at 50 DPI...
PDF converted to 1 images.
Processing image 1/1
  - No barcodes found.
  - Current memory usage: 0.78MB
  - Peak memory usage:   1.24MB
&lt;span class="nt"&gt;--------------------&lt;/span&gt;
Processing finished &lt;span class="k"&gt;in &lt;/span&gt;0.35 seconds.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As we can see, the peak memory usage drops significantly as we lower the DPI value. At 50 DPI, we are not able to detect the QR code anymore, but at 80 DPI, we are still able to read it and the peak memory usage is only 1.96MB, which is much more reasonable. My further experiments show that 80 unfortunately is too low a value, as even slightly smaller QR codes are not picked up anymore. Depending on your use case, the DPI of 100-150 could be reasonable, but for us, the sweet spot turned out to be somewhere around 220 DPI. Which still saves us about 1.5 second of processing time and nearly 6 times less memory usage compared to the original 500 DPI setting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about pre-processing?
&lt;/h2&gt;

&lt;p&gt;Changing the resolution was a great fix and a quick hotfix to change the resolution in production definitely saved us on some computing costs, but why not try to optimize even further? There are 2 optimizations that immediately come to mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scale the image down&lt;/li&gt;
&lt;li&gt;convert the image to grayscale&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since grayscaling the image turned out to be a very non-invasive change, I decided to bundle it together with the resizing in a single parameter called &lt;code&gt;--optimize&lt;/code&gt;. Please notice that I use the &lt;a href="https://pillow.readthedocs.io/" rel="noopener noreferrer"&gt;Pillow&lt;/a&gt; library to handle the pre-processing, but the &lt;code&gt;pdftoppm&lt;/code&gt; utility does support both of those operations as well with the &lt;code&gt;-scale-to&lt;/code&gt; and &lt;code&gt;-gray&lt;/code&gt; parameters, so you should most likely prefer them. I just did this simply because I did not know about those parameters at the time of writing the script and I'm quite proficient with Pillow.&lt;/p&gt;

&lt;p&gt;Let's try to run the script with the &lt;code&gt;--optimize&lt;/code&gt; parameter on the original 500 DPI image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python ./zbar_process.py CCI10072025.pdf &lt;span class="nt"&gt;--optimize&lt;/span&gt;
Starting PDF processing &lt;span class="k"&gt;for &lt;/span&gt;CCI10072025.pdf at 500 DPI...
PDF converted to 1 images.
Processing image 1/1
  - Resizing image from 4015x5727 to 1024x768
  - Converting to grayscale
  - Found barcode: http://kamilmarut.com
  - Current memory usage: 0.78MB
  - Peak memory usage:   2.36MB
&lt;span class="nt"&gt;--------------------&lt;/span&gt;
Processing finished &lt;span class="k"&gt;in &lt;/span&gt;1.88 seconds.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is great! This pre-processing alone had bigger impact on the memory usage than the resolution change! The processing time is also slightly lower, although it appears changing the density of the image has had a much bigger impact here.&lt;/p&gt;

&lt;p&gt;Now, let's try to run pre-processing on the image with the resolution set to 100 DPI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python ./zbar_process.py CCI10072025.pdf &lt;span class="nt"&gt;--resolution&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100 &lt;span class="nt"&gt;--optimize&lt;/span&gt;
Starting PDF processing &lt;span class="k"&gt;for &lt;/span&gt;CCI10072025.pdf at 100 DPI...
PDF converted to 1 images.
Processing image 1/1
  - Resizing image from 803x1146 to 717x1024
  - Converting to grayscale
  - Found barcode: http://kamilmarut.com
  - Current memory usage: 0.78MB
  - Peak memory usage:   2.25MB
&lt;span class="nt"&gt;--------------------&lt;/span&gt;
Processing finished &lt;span class="k"&gt;in &lt;/span&gt;0.43 seconds.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's an extra overhead of &lt;code&gt;.02&lt;/code&gt; seconds in processing time compared to just the resolution change due to the extra pre-processing, but the peak memory usage is reduced even further. If you're optimizing for memory usage, you should definitely consider this pre-processing step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In the end, we decided to only go with the DPI parameter change and the grayscaling in a pre-processing step, as we didn't want to risk breaking processing for documents that we don't have in our test suite. Remember, when tweaking parameters such as image resolution and quality, the clean-room strategy gives you an idea of what you can save, but you should always test on a representative sample of your production data.&lt;/p&gt;

&lt;p&gt;And for anybody curious, this is as low as I could go with the optimizations before ZBar stopped detecting the QR code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python ./zbar_process.py CCI10072025.pdf &lt;span class="nt"&gt;--resolution&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;54 &lt;span class="nt"&gt;--optimize&lt;/span&gt; &lt;span class="nt"&gt;--save-image&lt;/span&gt;
Starting PDF processing &lt;span class="k"&gt;for &lt;/span&gt;CCI10072025.pdf at 54 DPI...
PDF converted to 1 images.
Processing image 1/1
  - Converting to grayscale
  - Saving processed image to CCI10072025.jpg
  - Found barcode: http://kamilmarut.com
  - Current memory usage: 0.78MB
  - Peak memory usage:   1.32MB
&lt;span class="nt"&gt;--------------------&lt;/span&gt;
Processing finished &lt;span class="k"&gt;in &lt;/span&gt;0.36 seconds.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this is how the final image looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Froxsybswctv898r4uizb.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Froxsybswctv898r4uizb.jpg" alt="Final optimized image" width="434" height="619"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sure, it's small and there are visible artifacts, but still quite readable if you ask me - I don't know why ZBar's complaining!&lt;/p&gt;

</description>
      <category>performance</category>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>Django admin tricks I commonly use</title>
      <dc:creator>Kamil Marut</dc:creator>
      <pubDate>Thu, 13 Mar 2025 14:00:00 +0000</pubDate>
      <link>https://dev.to/exler/django-admin-tricks-i-commonly-use-2imp</link>
      <guid>https://dev.to/exler/django-admin-tricks-i-commonly-use-2imp</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;The Django admin site is one of the best features that Django provides. It's useful during the development process to manage the database data in a simplified (or enhanced) manner, as well as in production environment, where it provides a simple interface for the content managers to manage the data.&lt;/p&gt;

&lt;p&gt;Despite its simplicity, the Django admin site is extensible, although not always in a very obvious way. Nevertheless, it is a common pattern across many projects, I've seen, to actually extend the Django admin site as much as possible if most of your application is CRUD based. I've seen this done to an extent that the admin site was exposed to public customers as the main interface to the application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Django admin tricks
&lt;/h2&gt;

&lt;p&gt;Here are a couple of Django tricks / hidden features that I commonly use to modify the Django admin page and make it even more powerful!&lt;/p&gt;

&lt;h3&gt;
  
  
  Disabling editing/deleting capabilities
&lt;/h3&gt;

&lt;p&gt;Usually you want to be able to modify according to their user permissions. Sometimes (and I see this in a lot of projects) you either roll out your own permission system and forget about the Django permissions, give content managers the &lt;code&gt;is_superuser&lt;/code&gt; flag or simply want to completely prohibit users from deleting certain objects, no matter what. In this case, overriding the &lt;code&gt;has_*_permission&lt;/code&gt; &lt;a href="https://docs.djangoproject.com/en/5.1/ref/contrib/admin/#django.contrib.admin.ModelAdmin.has_add_permission" rel="noopener noreferrer"&gt;(documentation)&lt;/a&gt; is what you want to do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.contrib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;admin&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomModelAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModelAdmin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;has_add_permission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Disables the possibility of adding a new object
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;has_change_permission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Disables the possibility of updating an object
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;has_delete_permission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Disables the possibility of deleting an object
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course, you can modify this to fit your requirements, like a separate permission system. But who has time for that, right?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhdcvp4d7inrgc89t592k.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhdcvp4d7inrgc89t592k.jpg" alt="Deadlines, deadlines everywhere" width="512" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding extra pages
&lt;/h3&gt;

&lt;p&gt;Django Admin is great because it's automatically generated based on the registered models. But sometimes you just need to squeeze this extra dashboard page or a form that doesn't really link with any of the models. In that case, let me introduce you &lt;code&gt;ModelAdmin.get_urls()&lt;/code&gt; &lt;a href="https://docs.djangoproject.com/en/5.1/ref/contrib/admin/#django.contrib.admin.ModelAdmin.get_urls" rel="noopener noreferrer"&gt;(documentation)&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.contrib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;admin&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.template.response&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TemplateResponse&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.urls&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomModelAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModelAdmin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_urls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get_urls&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="c1"&gt;# Custom URL list
&lt;/span&gt;        &lt;span class="c1"&gt;# Make sure to wrap your view in `self.admin_site.admin_view()`
&lt;/span&gt;        &lt;span class="c1"&gt;# to avoid caching the view (unless you want it to be cached, then you can use `cacheable=True`)
&lt;/span&gt;        &lt;span class="c1"&gt;# and to have proper permission checks in place.
&lt;/span&gt;        &lt;span class="n"&gt;my_urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my_view/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;admin_site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;admin_view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;my_view&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;my_urls&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;my_view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="c1"&gt;# Include common variables for rendering the admin template.
&lt;/span&gt;                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;admin_site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="c1"&gt;# Your custom context
&lt;/span&gt;                &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;TemplateResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sometemplate.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Handle POST requests in case you have a form or want to take an action
&lt;/span&gt;            &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And your custom template should most likely inherit from the &lt;code&gt;admin/base_site.html&lt;/code&gt; template, to be consistent with the rest of the admin site.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;{% extends "admin/base_site.html" %}
{% block content %}
...
{% endblock %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you have a page ready that you can navigate to and perform operations on. But how to navigate to that page, you ask? Well, now it's only navigable by typing the URL directly, but you can modify the &lt;code&gt;change_form.html&lt;/code&gt; and &lt;code&gt;change_list.html&lt;/code&gt; templates to include a link to your custom page.&lt;/p&gt;

&lt;p&gt;Here's an example on the &lt;code&gt;change_form.html&lt;/code&gt; template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;{% extends "admin/change_form.html" %}
{% block object-tools-items %}
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"{% url 'admin:fill-automagically-'|add:opts.app_label|add:'-'|add:opts.model_name original.pk %}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Fill Automagically&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    {{ block.super }}
{% endblock object-tools-items %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb00w50xxq6n6o8qm3q7k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb00w50xxq6n6o8qm3q7k.png" alt="New button on the change form" width="517" height="73"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And here's an example from another project on the &lt;code&gt;change_list.html&lt;/code&gt; template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;{% extends "admin/change_list.html" %}
{% block object-tools-items %}
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"background: #ffffbb; color: #000"&lt;/span&gt; 
            &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"{% url 'admin:accommodations-import' %}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Zaimportuj CSV&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    {{ block.super }}
{% endblock object-tools-items %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fworyldrtuct1u8qroymd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fworyldrtuct1u8qroymd.png" alt="New button on the change list" width="610" height="96"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;They are both very similar, but we extend other base templates and in the first example, we use the app label and model name to reverse the URL. If you want your template to be more reusable, then this would be the recommended approach. Otherwise, the second one is simpler.&lt;/p&gt;

&lt;p&gt;You can either override the templates by using the same names as per &lt;a href="https://docs.djangoproject.com/en/5.1/ref/contrib/admin/#templates-which-may-be-overridden-per-app-or-model" rel="noopener noreferrer"&gt;the Django documentation&lt;/a&gt;, or you can specify the templates in the &lt;code&gt;ModelAdmin&lt;/code&gt; class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomModelAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModelAdmin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;change_list_template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yourapp/admin/custom_change_list.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;change_form_template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yourapp/admin/custom_change_form.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Improving performance with &lt;code&gt;raw_id_fields&lt;/code&gt;, &lt;code&gt;list_select_related&lt;/code&gt; and &lt;code&gt;autocomplete_fields&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Even if your admin panel is not customer facing, it does not mean that you should not care about performance. Quite the opposite actually! It would be a shame to see your admin panel become completely unusable or block a worker for an extended period of time because you forgot to get rid of those pesky N+1 queries. This is why it's important to know how to manage the performance of your admin panel.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;raw_id_fields&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;By default, Django provides you with a select-box interface for your &lt;code&gt;ForeignKey&lt;/code&gt; fields, filled with all the objects from the other table. Not only is this not always useful (are you really going to hand-select an item from a select-box containing a couple of millions of items?) it also means you have to fetch all the related instances and render them. This oftentimes results in a timeout, especially if you have a couple of those fields on the same page.&lt;/p&gt;

&lt;p&gt;By using &lt;code&gt;raw_id_fields&lt;/code&gt; &lt;a href="https://docs.djangoproject.com/en/5.1/ref/contrib/admin/#django.contrib.admin.ModelAdmin.raw_id_fields" rel="noopener noreferrer"&gt;(documentation)&lt;/a&gt;, you only show the actual foreign key value (in most cases the ID) and a representation of the object (if it's filled out and saved).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomModelAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModelAdmin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;raw_id_fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;code&gt;list_select_related&lt;/code&gt; or &lt;code&gt;get_list_select_related&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;Your admin page can also show values from the related models. While this lookup is fairly quick on the detail page, on the list page it can quickly deteriorate the performance of your admin panel by introducing an &lt;a href="https://adamj.eu/tech/2020/09/01/django-and-the-n-plus-one-queries-problem/" rel="noopener noreferrer"&gt;N+1 query problem&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;By using &lt;code&gt;list_select_related&lt;/code&gt; &lt;a href="https://docs.djangoproject.com/en/5.1/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_select_related" rel="noopener noreferrer"&gt;(documentation)&lt;/a&gt;, you can specify which fields should have their related-object data fetched in the same query as the main object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomModelAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModelAdmin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;list_select_related&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Or if you need to add some conditions
&lt;/span&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_list_select_related&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_superuser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Maybe the user sees more fields than the regular staff user
&lt;/span&gt;            &lt;span class="c1"&gt;# so we fetch more related objects
&lt;/span&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extra_obj&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;code&gt;autocomplete_fields&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;Remember &lt;code&gt;raw_id_fields&lt;/code&gt; from before? It's great if you mostly use the admin field to view the value or know the IDs of the objects. But what if you're trying to edit the field, and you need to look up the object? Enter &lt;code&gt;autocomplete_fields&lt;/code&gt; &lt;a href="https://docs.djangoproject.com/en/5.1/ref/contrib/admin/#django.contrib.admin.ModelAdmin.autocomplete_fields" rel="noopener noreferrer"&gt;(documentation)&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;This replaces the select-box with a Select2 autocomplete input, that you can search in and it asynchronously fetches the related objects. Great for performance AND usability!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomModelAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModelAdmin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;autocomplete_fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Hopefully this article gave you an idea on how the Django admin panel can be extended and adjusted to your specific needs. Make sure to read through the &lt;a href="https://docs.djangoproject.com/en/5.1/ref/contrib/admin/" rel="noopener noreferrer"&gt;Django documentation on the admin site&lt;/a&gt; to learn more about cool features like &lt;a href="https://docs.djangoproject.com/en/5.1/ref/contrib/admin/#inlinemodeladmin-objects" rel="noopener noreferrer"&gt;inline admin&lt;/a&gt; or &lt;a href="https://docs.djangoproject.com/en/5.1/ref/contrib/admin/#adding-a-password-reset-feature" rel="noopener noreferrer"&gt;password reset for admin users&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>django</category>
    </item>
    <item>
      <title>Dockerfile for Django Devs</title>
      <dc:creator>Kamil Marut</dc:creator>
      <pubDate>Fri, 07 Mar 2025 10:00:00 +0000</pubDate>
      <link>https://dev.to/exler/dockerfile-for-django-devs-5bml</link>
      <guid>https://dev.to/exler/dockerfile-for-django-devs-5bml</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I absolutely adore Django. It's a fantastic web framework that allows you to prototype quickly while also being powerful enough to scale into production and beyond.&lt;/p&gt;

&lt;p&gt;Equally, I love Docker. It has made my life so much easier—both as a developer and as a DevOps engineer.  &lt;/p&gt;

&lt;p&gt;Most of my Django projects are containerized, and setting up a Dockerfile is one of the first things I do when starting a new project. Since I don’t usually revisit it until later stages of development, I find it essential to have a solid, reusable template.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbooxlze1lbw6aov9vs58.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbooxlze1lbw6aov9vs58.jpg" width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Dockerfile and its components
&lt;/h2&gt;

&lt;p&gt;This is a Dockerfile that I usually start with. It is a multi-stage build that expects that you use &lt;a href="https://docs.astral.sh/uv/" rel="noopener noreferrer"&gt;uv&lt;/a&gt; for managing dependencies (which you should, it's great) and PostgreSQL as your database (which you probably do anyway). Otherwise, it is quite easy to adapt it to other project managers like Poetry and remove the PostgreSQL dependencies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Builder stage&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.13-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PYTHONUNBUFFERED=1&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PYTHONDONTWRITEBYTECODE=1&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; UV_VERSION=0.6.4&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    build-essential &lt;span class="se"&gt;\
&lt;/span&gt;    libpq-dev &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; pip &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"uv==&lt;/span&gt;&lt;span class="nv"&gt;$UV_VERSION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; pyproject.toml uv.lock /app/&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;uv &lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; requirements-txt &lt;span class="nt"&gt;--no-hashes&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /app/requirements.txt
&lt;span class="k"&gt;RUN &lt;/span&gt;pip wheel &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;--no-deps&lt;/span&gt; &lt;span class="nt"&gt;--wheel-dir&lt;/span&gt; /app/wheels &lt;span class="nt"&gt;-r&lt;/span&gt; /app/requirements.txt

&lt;span class="c"&gt;# Final stage&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.13-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;final&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PYTHONUNBUFFERED=1&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PYTHONDONTWRITEBYTECODE=1&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/wheels /wheels&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/requirements.txt /tmp/requirements.txt&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;--no-index&lt;/span&gt; &lt;span class="nt"&gt;--find-links&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/wheels &lt;span class="nt"&gt;-r&lt;/span&gt; /tmp/requirements.txt &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /wheels /tmp/requirements.txt

&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    libpq-dev &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;useradd &lt;span class="nt"&gt;-U&lt;/span&gt; appuser &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 &lt;span class="nt"&gt;-o&lt;/span&gt; appuser &lt;span class="nt"&gt;-g&lt;/span&gt; appuser /app/staticfiles &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 &lt;span class="nt"&gt;-o&lt;/span&gt; appuser &lt;span class="nt"&gt;-g&lt;/span&gt; appuser /app/media

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=appuser:appuser . .&lt;/span&gt;

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser:appuser&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;python manage.py collectstatic &lt;span class="nt"&gt;--noinput&lt;/span&gt;

&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; [ "/app/entrypoint.sh" ]&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; [ "server" ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's continue by breaking down the Dockerfile line-by-line.&lt;/p&gt;

&lt;h3&gt;
  
  
  Builder stage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.13-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This line sets the base image for the builder stage. This example uses Python 3.13, but of course this version varies between the projects. &lt;br&gt;
What rarely changes, though, is that I use the slim version of the image. &lt;br&gt;
This is because it is smaller, and I have a preference for installing all the dependencies myself; so far I've never had a problem with the slim version.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PYTHONUNBUFFERED=1&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PYTHONDONTWRITEBYTECODE=1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setting the &lt;code&gt;PYTHONUNBUFFERED&lt;/code&gt; environment variable ensures that the Python output is sent straight to the terminal without buffering it first. &lt;br&gt;
If you don't set this, you might have a delay in seeing the output of your application.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PYTHONDONTWRITEBYTECODE&lt;/code&gt; prevents Python from writing &lt;code&gt;.pyc&lt;/code&gt; files to the disk, &lt;br&gt;
which might reduce the image size if you run the application during build time &lt;br&gt;
and potentially help you avoid some issues with the cache.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; UV_VERSION=0.6.4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This line sets the version of &lt;code&gt;uv&lt;/code&gt; that we want to use later in the build process. &lt;br&gt;
Usually you would want to pin the version of &lt;code&gt;uv&lt;/code&gt; or whatever dependency manager you're using to ensure you have the same dependency resolver version that you've tested with.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    build-essential &lt;span class="se"&gt;\
&lt;/span&gt;    libpq-dev &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; pip &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"uv==&lt;/span&gt;&lt;span class="nv"&gt;$UV_VERSION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We finally get to the dependency installation part. &lt;br&gt;
First we update the package list to have the latest package versions available, install &lt;code&gt;build-essential&lt;/code&gt; which is nearly always required for compiling other dependencies, and &lt;code&gt;libpq-dev&lt;/code&gt; which is required for compiling &lt;code&gt;psycopg&lt;/code&gt; (the PostgreSQL adapter for Python).&lt;br&gt;
Then we upgrade &lt;code&gt;pip&lt;/code&gt; to the latest version and install &lt;code&gt;uv&lt;/code&gt; with the version we set earlier.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; pyproject.toml uv.lock /app/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We are copying the &lt;code&gt;pyproject.toml&lt;/code&gt; and &lt;code&gt;uv.lock&lt;/code&gt; files to the &lt;code&gt;/app/&lt;/code&gt; directory in the container. &lt;br&gt;
Those are the only files we need from the project that we need in the builder stage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;uv &lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; requirements-txt &lt;span class="nt"&gt;--no-hashes&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /app/requirements.txt
&lt;span class="k"&gt;RUN &lt;/span&gt;pip wheel &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;--no-deps&lt;/span&gt; &lt;span class="nt"&gt;--wheel-dir&lt;/span&gt; /app/wheels &lt;span class="nt"&gt;-r&lt;/span&gt; /app/requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These two lines are the most important in the builder stage.&lt;br&gt;
The first line exports the dependencies to a &lt;code&gt;requirements.txt&lt;/code&gt; file. This ensures we have a file that we can work with using &lt;code&gt;pip&lt;/code&gt; in the next line. &lt;br&gt;
Additionally, we use the &lt;code&gt;--no-hashes&lt;/code&gt; flag to ensure that the hashes are not included in the file. This is optional, and generally I would recommend to first try to build without the flag. &lt;br&gt;
I often run into various problems when using the hashes, so I'm mentioning it here as it shows up in my Dockerfiles often.&lt;/p&gt;

&lt;p&gt;The second line installs the dependencies into the &lt;code&gt;/app/wheels&lt;/code&gt; directory. &lt;br&gt;
This ensures that all required packages are pre-built in a wheel format. &lt;br&gt;
We can then use these wheels in the final stage to avoid having to install certain compile dependencies, as well as to speed up the process.&lt;/p&gt;
&lt;h3&gt;
  
  
  Final stage
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.13-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;final&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This line sets the base image for the final stage. It is quite important to use the same version of Python as in the builder stage,&lt;br&gt;
so you could go ahead and pin the version of the Python image as a build argument, for example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PYTHONUNBUFFERED=1&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PYTHONDONTWRITEBYTECODE=1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing new here. We set the same environment variables as in the builder stage for the same reasons.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/wheels /wheels&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/requirements.txt /tmp/requirements.txt&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;--no-index&lt;/span&gt; &lt;span class="nt"&gt;--find-links&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/wheels &lt;span class="nt"&gt;-r&lt;/span&gt; /tmp/requirements.txt &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /wheels /tmp/requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These lines copy the compiled Python dependencies and the &lt;code&gt;requirements.txt&lt;/code&gt; file from the builder stage to the final stage. &lt;br&gt;
Then we install the dependencies from the wheels directory. We also remove the wheels and the &lt;code&gt;requirements.txt&lt;/code&gt; file to keep the image size down.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    libpq-dev &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we can install all the runtime dependencies that we need. In case you're not running PostgreSQL as your database, you could remove this completely.&lt;br&gt;
In case you need more runtime dependencies, you would specify them here. We also remove the package list to keep the image size down.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;useradd &lt;span class="nt"&gt;-U&lt;/span&gt; appuser &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 &lt;span class="nt"&gt;-o&lt;/span&gt; appuser &lt;span class="nt"&gt;-g&lt;/span&gt; appuser /app/staticfiles &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 &lt;span class="nt"&gt;-o&lt;/span&gt; appuser &lt;span class="nt"&gt;-g&lt;/span&gt; appuser /app/media
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is recommended to run Docker containers as a non-root user. &lt;br&gt;
So we create a user called &lt;code&gt;appuser&lt;/code&gt; and create the directories for the static files and media files with the correct permissions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=appuser:appuser . .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We set the working directory to &lt;code&gt;/app&lt;/code&gt; and copy the actual project files to the container.&lt;br&gt;
We also set the owner of the files to &lt;code&gt;appuser&lt;/code&gt; to ensure we don't have any permission issues.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser:appuser&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This line sets the user to &lt;code&gt;appuser&lt;/code&gt; so that the application runs as this user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;python manage.py collectstatic &lt;span class="nt"&gt;--noinput&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Controversial, but if you are serving the static files from the Django application (e.g. using Whitenoise), it makes sense to embed the static files in the image.&lt;br&gt;
This way, you don't have to worry about running &lt;code&gt;collectstatic&lt;/code&gt; manually or in post-build script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; [ "/app/entrypoint.sh" ]&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; [ "server" ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This line sets the entrypoint for the container and runs the &lt;code&gt;server&lt;/code&gt; command by default. I will elaborate on the &lt;code&gt;entrypoint.sh&lt;/code&gt; script in the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Entrypoint
&lt;/h2&gt;

&lt;p&gt;This is the &lt;code&gt;entrypoint.sh&lt;/code&gt; script that I usually use. It is quite simple and only accepts the &lt;code&gt;server&lt;/code&gt; command as an argument.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nv"&gt;PROCESS_TYPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Running process type: &lt;/span&gt;&lt;span class="nv"&gt;$PROCESS_TYPE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROCESS_TYPE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"server"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;exec &lt;/span&gt;gunicorn &lt;span class="nt"&gt;--bind&lt;/span&gt; 0.0.0.0:8000 &lt;span class="nt"&gt;--log-level&lt;/span&gt; INFO &lt;span class="nt"&gt;--access-logfile&lt;/span&gt; &lt;span class="s2"&gt;"-"&lt;/span&gt; &lt;span class="nt"&gt;--error-logfile&lt;/span&gt; &lt;span class="s2"&gt;"-"&lt;/span&gt; app.wsgi
&lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Unknown process type: &lt;/span&gt;&lt;span class="nv"&gt;$PROCESS_TYPE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the very basic scenario, you could definitely just replace it with the &lt;code&gt;CMD&lt;/code&gt; instruction in the Dockerfile, but I like to keep it separate for flexibility.&lt;/p&gt;

&lt;p&gt;This flexibility comes useful quickly if you would like to Dockerize a Celery worker or a management command, for example.&lt;br&gt;
You could also add more commands to the script, like running migrations before starting the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;There's really no one-size-fits-all Dockerfile for Django projects, but this is as close as I've gotten to something that works most of the time&lt;br&gt;
and if it doesn't, it's still a great starting point.  &lt;/p&gt;

</description>
      <category>docker</category>
      <category>django</category>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>Simplest Homelab Backup Strategy (That Came to My Mind)</title>
      <dc:creator>Kamil Marut</dc:creator>
      <pubDate>Tue, 04 Mar 2025 12:44:14 +0000</pubDate>
      <link>https://dev.to/exler/simplest-homelab-backup-strategy-that-came-to-my-mind-13nj</link>
      <guid>https://dev.to/exler/simplest-homelab-backup-strategy-that-came-to-my-mind-13nj</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;As a self-hosting enthusiast, I try to replace as many services as possible with their self-hosted counterparts. &lt;br&gt;
I also try to avoid making the same mistake as many other self-hosting enthusiasts before me—running a setup without a proper backup solution in place. &lt;/p&gt;

&lt;p&gt;I've been lucky enough not to lose any data (and from what I know, I've really been playing with fire, especially since I run some services on a Raspberry Pi with an SD card). But I've decided I don't want to rely on luck. Instead of having &lt;em&gt;20/20 hindsight&lt;/em&gt;, I want &lt;em&gt;20/20 foresight&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl63e3t5cyipqiupa8rjk.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl63e3t5cyipqiupa8rjk.jpg" alt="Back up yourselves, data loss is coming." width="691" height="361"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;My setup is still pretty modest, as I'm still experimenting to achieve the right mix of hosted and self-hosted services.&lt;/p&gt;

&lt;p&gt;I have a Raspberry Pi 5 that runs multiple services using Docker. I manage the Docker services using Docker Compose and &lt;a href="https://github.com/louislam/dockge" rel="noopener noreferrer"&gt;Dockge&lt;/a&gt;. &lt;br&gt;
I've used Portainer in the past, and while I liked working with it, I found it to be overkill for a single-server setup.&lt;/p&gt;

&lt;p&gt;Additionally, I have a Synology DS220+ NAS server that I use for real-time device sync using Synology Drive and as a media server using Jellyfin. &lt;br&gt;
Synology NAS is running in a RAID 1 configuration and uses Cloud Sync to store an encrypted copy of the data on a remote server for extra safety.&lt;/p&gt;

&lt;p&gt;Both of the devices are on the same network, but I use Tailscale to connect between different machines as I like being independent of the network setup.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Strategy
&lt;/h2&gt;

&lt;p&gt;The plan is to do a daily backup of the data from the Pi to the Synology NAS. &lt;/p&gt;

&lt;p&gt;I explored different options on how to effectively and, more importantly, securely backup the data. &lt;br&gt;
I first wanted to use &lt;a href="https://linux.die.net/man/1/rsync" rel="noopener noreferrer"&gt;rsync&lt;/a&gt; to copy the data from the Pi to the NAS, but I didn't like opening the SSH port on the NAS. &lt;/p&gt;

&lt;p&gt;The next best thing that came to mind was using NFS on a single shared directory, mounting it on the Pi, and then backing up the data to the NFS share.&lt;/p&gt;

&lt;p&gt;I've set up a shared directory on the Synology NAS and created the following NFS rule for it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fej6s30ejyg2iv2gzwkb2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fej6s30ejyg2iv2gzwkb2.png" alt="Synology NFS Rule Configuration" width="800" height="546"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This config allows me to mount the directory through the Tailscale network (and not through the local network) &lt;/p&gt;

&lt;p&gt;The two main "gotchas" in my setup are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Enable the "Allow connections from non-privileged ports" option.&lt;/p&gt;

&lt;p&gt;I tried forcing the NFS mount to use a privileged port, but I was constantly getting "access denied" errors. I don't have any proof for this, but I think it might be related to Tailscale modifying the network traffic. I decided to enable this option as the NFS share is only accessible through the Tailscale network, so it should be secure enough.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use &lt;code&gt;127.0.0.1&lt;/code&gt; as the client IP address if you try to connect through Tailscale. &lt;/p&gt;

&lt;p&gt;At first I was using the Tailscale IP first and that failed. Then I switched to the local IP and that worked. &lt;br&gt;
I was pulling my hair out until I found &lt;a href="https://www.reddit.com/r/Tailscale/comments/p09wrh/cant_nas_mount_synology_nas/" rel="noopener noreferrer"&gt;this Reddit thread&lt;/a&gt; that pointed out that the Tailscale traffic goes through the loopback interface. Thanks, Internet stranger!&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now that I have the NFS share set up, I need to mount it on the Pi. I started by running the following commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create the mount point&lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; /mnt/backup
&lt;span class="c"&gt;# Mount the NFS share&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;-t&lt;/span&gt; nfs &amp;lt;tailscale-ip&amp;gt;:/volume1/backup_machines /mnt/backup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If successful, then no message is shown. You can confirm the mount by running &lt;code&gt;ls /mnt/backup&lt;/code&gt; and seeing the files from the NAS or by running &lt;code&gt;mount | grep nfs&lt;/code&gt; to see if the directory is mounted correctly.&lt;/p&gt;

&lt;p&gt;Of course, we don't want to run this command every time we restart the Pi. So I added the following line to the &lt;code&gt;/etc/fstab&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;tailscale-ip&amp;gt;:/volume1/backup_machines /mnt/backup nfs defaults 0 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a perfect world, this would probably do the trick. But unfortunately, Tailscale once again adds a twist to the story. The Tailscale service is not started when the &lt;code&gt;/etc/fstab&lt;/code&gt; is read, so the mount fails. The simplest solution I could think of is to add a cron job that runs the mount command after a reboot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Run crontab as root
sudo crontab -e

# Add the following line
@reboot mount -a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fortunately at the point that this cronjob runs, the Tailscale service is already started and the NFS share is mounted correctly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhx003zxdpqs1imptt9wf.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhx003zxdpqs1imptt9wf.gif" alt="Hell Yeah" width="500" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Perfect! We now have the NFS share mounted on the Pi. &lt;br&gt;
Now, we need to copy the data over there. But where do we copy it from? For each of the services, I store the relevant data in Docker persistent volumes. They are technically available in &lt;code&gt;/var/lib/docker/volumes&lt;/code&gt;, but it is generally not recommended to access them directly. So I once again dived in search of a better way to backup those volumes. &lt;/p&gt;

&lt;p&gt;I came across this amazing project &lt;a href="https://github.com/offen/docker-volume-backup" rel="noopener noreferrer"&gt;offen/docker-volume-backup&lt;/a&gt; which not only can backup volumes locally on a schedule, it can also gracefully shut down the containers before doing so to make sure there are no unfinished writes. It also supports features like notifications, backup rotation, and compression. Additionally, it's compatible with different storage backends, making it easy to switch strategies—for example, to a remote server. And all of this with a simple Docker container. This is the whole Docker Compose configuration I use for it, not much more than the example from the project's docs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;backup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;offen/docker-volume-backup:v2.43.2&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# Binding the volumes to be backed up to the container directory being backed up&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;volume1:/backup/volume1:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;volume2:/backup/volume2:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;volume3:/backup/volume3:ro&lt;/span&gt;
      &lt;span class="c1"&gt;# Binding the Docker socket to the container to be able to gracefully stop the containers during the backup&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock:ro&lt;/span&gt;
      &lt;span class="c1"&gt;# Binding the NFS share to the container directory where the backups are stored&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/mnt/backup:/archive&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;volume1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Marking as externals as volumes are created in other compose files&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;volume2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;volume3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I set it up to compress and backup the volumes to the mounted NFS share on the NAS every night with an e-mail notification if something goes wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Run the backup everyday at 3:30
BACKUP_CRON_EXPRESSION="30 3 * * *"

BACKUP_FILENAME="backup-%Y-%m-%dT%H-%M-%S.{{ .Extension }}"

# Compress the backup
BACKUP_COMPRESSION="gz"
GZIP_PARALLELISM=1

NOTIFICATION_URLS=smtp://...
NOTIFICATION_LEVEL="error"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Et voilà! We can manually trigger a backup to confirm it's working:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;backup backup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And see the files on the NAS:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdlbn34o1qu8gnx0qx7u9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdlbn34o1qu8gnx0qx7u9.png" alt="Backup Success" width="712" height="89"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Even though it took me a while to set this up, I see this as a one-time investment as I had to learn a bit more about NFS and Tailscale. This setup works well now (remember to test your backups!) but I'm definitely going to explore other possibilities as I expand my homelab.&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>selfhost</category>
      <category>docker</category>
      <category>backup</category>
    </item>
    <item>
      <title>Diagrams as Code (DaC) - crafting beautiful diagrams with D2</title>
      <dc:creator>Kamil Marut</dc:creator>
      <pubDate>Thu, 01 Dec 2022 00:00:00 +0000</pubDate>
      <link>https://dev.to/exler/diagrams-as-code-dac-crafting-beautiful-diagrams-with-d2-1nd4</link>
      <guid>https://dev.to/exler/diagrams-as-code-dac-crafting-beautiful-diagrams-with-d2-1nd4</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftkpo3xpj6hs3u1vycawu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftkpo3xpj6hs3u1vycawu.png" alt="Cover" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We all love diagrams. A quick peek allows us to quickly create a mental map, no matter whether it's cloud architecture, a database schema, or a sequence diagram. &lt;br&gt;
But creating diagrams is hard. It's hard to keep them up to date, it's hard to collaborate on them, and it's hard to make them look good.&lt;/p&gt;

&lt;p&gt;There are a lot of tools for creating diagrams, but probably the most popular one is &lt;a href="https://draw.io" rel="noopener noreferrer"&gt;draw.io&lt;/a&gt;. It's a great tool, but it's not without its flaws.&lt;br&gt;
It gives you a lot of freedom, but also bothers you with the little details.&lt;/p&gt;

&lt;p&gt;Since we often use diagrams to describe our infrastructure, why don't we learn from the DevOps world and treat them as code? &lt;br&gt;
What if next to our Infrastructure as Code (IaC) we could also have Diagrams as Code (DaC)?&lt;/p&gt;
&lt;h2&gt;
  
  
  What is D2?
&lt;/h2&gt;

&lt;p&gt;In the words of the creators, &lt;em&gt;D2 is a domain-specific language (DSL) that stands for Declarative Diagramming. Declarative, as in, you describe what you want diagrammed, it generates the image.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In other words, D2 gives you the ability to write diagrams as code, and then use a CLI tool to compile and render them into SVG or PNG images.&lt;br&gt;
The CLI tool supports live reload, so the workflow is nearly as smooth as working with other diagram tools.&lt;/p&gt;
&lt;h2&gt;
  
  
  Get started with D2
&lt;/h2&gt;

&lt;p&gt;You can install the D2 CLI tools by following the instructions on the &lt;a href="https://d2lang.com/tour/install" rel="noopener noreferrer"&gt;D2 website&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;After installing the CLI, you should now be good to go. The most important command is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ d2 -w input.d2 output.svg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will run a live reload server that will watch for changes in the &lt;code&gt;input.d2&lt;/code&gt; file and render the output to &lt;code&gt;output.svg&lt;/code&gt;.&lt;br&gt;
It will also open a browser window with the rendered diagram. For most cases this is all you need, however it is also worth noting&lt;br&gt;
that you can change the theme of the diagram by passing the &lt;code&gt;--theme/-t&lt;/code&gt; parameter. This parameter is the ID of the target theme, which is not&lt;br&gt;
showcased in the D2 docs, but you can find a list of all available themes in the &lt;a href="https://github.com/terrastruct/d2/tree/master/d2themes" rel="noopener noreferrer"&gt;D2 repo&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Renders with the "Shirley temple" theme
$ d2 -w input.d2 -t 102 output.svg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Creating a diagram with D2
&lt;/h2&gt;

&lt;p&gt;Using D2 to create a diagram is very simple. To present this, we will try to recreate the example AWS architecture diagram from &lt;a href="https://draw.io" rel="noopener noreferrer"&gt;draw.io&lt;/a&gt; examples.&lt;/p&gt;

&lt;p&gt;&lt;a href="/posts/diagrams-as-code/drawio.png" class="article-body-image-wrapper"&gt;&lt;img src="/posts/diagrams-as-code/drawio.png" alt="Example AWS architecture diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We start by defining the containers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws_1: AWS Cloud {}

aws_2: AWS Cloud {}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;aws_1&lt;/code&gt; and &lt;code&gt;aws_2&lt;/code&gt; are the IDs of the containers. We can then later use those IDs to reference the objects inside (or the containers themselves).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fctefd22i951vynacb10s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fctefd22i951vynacb10s.png" alt="Diagram with defined containers" width="136" height="228"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, we define the objects inside the containers (and the nested containers).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws_1: AWS Cloud {
  obj: Object
  bucket_1: Bucket
  bucket_2: Bucket

  subsystem: "" {
    cloudwatch: Amazon CloudWatch
    cloudtrail: Amazon CloudTrail
    sns: Amazon Simple Notification Service
    sqs: Amazon Simple Queue Service
    lambda: AWS Lambda
    kinesis: Amazon Kinesis Data Firehose
    dynamo: Amazon DynamoDB
  }
}

aws_2: AWS Cloud {
  bucket: Bucket

  subsystem: "" {
    cloudwatch: Amazon CloudWatch
    cloudtrail: Amazon CloudTrail
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcv5hpheiniwrqny04lt5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcv5hpheiniwrqny04lt5.png" alt="Diagram with defined objects" width="800" height="644"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We have defined all the objects inside the containers. Now we can make the connections between them.&lt;br&gt;
This is by far the part that is most troublesome when using traditional diagram tools, since with each update of the diagram you have to make sure&lt;br&gt;
that the connections are still clear and correct. In D2, this is a breeze.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws_1.bucket_1 -&amp;gt; aws_2.bucket
aws_2.subsystem.cloudwatch -&amp;gt; aws_1.subsystem.cloudwatch: Monitor template

aws_1: AWS Cloud {
  obj: Object
  bucket_1: Bucket
  bucket_2: Bucket

  obj -&amp;gt; bucket_1
  bucket_1 -&amp;gt; subsystem.cloudtrail
  subsystem.kinesis -&amp;gt; bucket_2

  subsystem: "" {
    cloudwatch: Amazon CloudWatch
    cloudtrail: Amazon CloudTrail
    sns: Amazon Simple Notification Service
    sqs: Amazon Simple Queue Service
    lambda: AWS Lambda
    kinesis: Amazon Kinesis Data Firehose
    dynamo: Amazon DynamoDB

    cloudtrail -&amp;gt; cloudwatch -&amp;gt; sns -&amp;gt; sqs -&amp;gt; lambda
    lambda -&amp;gt; dynamo
    lambda -&amp;gt; kinesis
  }
}

aws_2: AWS Cloud {
  bucket: Bucket

  bucket -&amp;gt; subsystem.cloudtrail

  subsystem: "" {
    cloudwatch: Amazon CloudWatch
    cloudtrail: Amazon CloudTrail

    cloudtrail -&amp;gt; cloudwatch
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnzcgxsm98fp1xqbo8ksb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnzcgxsm98fp1xqbo8ksb.png" alt="Diagram with defined connections" width="800" height="798"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And that's it! The specified objects are connected automatically. But the diagram still looks a little raw - let's add some icons to make it look better.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws_1.bucket_1 -&amp;gt; aws_2.bucket
aws_2.subsystem.cloudwatch -&amp;gt; aws_1.subsystem.cloudwatch: Monitor template

aws_1: AWS Cloud {
    obj: Object
    obj.shape: circle
    obj.style.stroke: "#62a638"
    bucket_1: Bucket
    bucket_1.shape: image
    bucket_1.icon: https://raw.githubusercontent.com/exler/diagrams/main/icons/aws/s3/s3.svg
    bucket_2: Bucket
    bucket_2.shape: image
    bucket_2.icon: https://raw.githubusercontent.com/exler/diagrams/main/icons/aws/s3/s3.svg

    obj -&amp;gt; bucket_1
    bucket_1 -&amp;gt; subsystem.cloudtrail
    subsystem.kinesis -&amp;gt; bucket_2

    subsystem: "" {
        cloudwatch: Amazon CloudWatch
        cloudwatch.shape: image
        cloudwatch.icon: https://raw.githubusercontent.com/exler/diagrams/main/icons/aws/cloudwatch/cloudwatch.svg
        cloudtrail: Amazon CloudTrail
        cloudtrail.shape: image
        cloudtrail.icon: https://raw.githubusercontent.com/exler/diagrams/main/icons/aws/cloudtrail/cloudtrail.svg
        sns: Amazon Simple Notification Service
        sns.shape: image
        sns.icon: https://raw.githubusercontent.com/exler/diagrams/main/icons/aws/simple_notification_service/sns.svg
        sqs: Amazon Simple Queue Service
        sqs.shape: image
        sqs.icon: https://raw.githubusercontent.com/exler/diagrams/main/icons/aws/simple_queue_service/sqs.svg
        lambda: AWS Lambda
        lambda.shape: image
        lambda.icon: https://raw.githubusercontent.com/exler/diagrams/main/icons/aws/lambda/lambda.svg
        kinesis: Amazon Kinesis Data Firehose
        kinesis.shape: image
        kinesis.icon: https://raw.githubusercontent.com/exler/diagrams/main/icons/aws/kinesis_data_firehose/firehose.svg
        dynamo: Amazon DynamoDB
        dynamo.shape: image
        dynamo.icon: https://raw.githubusercontent.com/exler/diagrams/main/icons/aws/dynamodb/dynamodb.svg

        cloudtrail -&amp;gt; cloudwatch -&amp;gt; sns -&amp;gt; sqs -&amp;gt; lambda
        lambda -&amp;gt; dynamo
        lambda -&amp;gt; kinesis
    }
}

aws_2: AWS Cloud {
    bucket: Bucket
    bucket.shape: image
    bucket.icon: https://raw.githubusercontent.com/exler/diagrams/main/icons/aws/s3/s3.svg

    bucket -&amp;gt; subsystem.cloudtrail

    subsystem: "" {
        cloudwatch: Amazon CloudWatch
        cloudwatch.shape: image
        cloudwatch.icon: https://raw.githubusercontent.com/exler/diagrams/main/icons/aws/cloudwatch/cloudwatch.svg
        cloudtrail: Amazon CloudTrail
        cloudtrail.shape: image
        cloudtrail.icon: https://raw.githubusercontent.com/exler/diagrams/main/icons/aws/cloudtrail/cloudtrail.svg

        cloudtrail -&amp;gt; cloudwatch
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the icons added, the diagram is much more intuitive and easier to understand. Sadly, D2 does not support filesystem paths for icons, so you have to use remote URLs.&lt;br&gt;
Nevertheless, the diagram is finished and looks great.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhahrhgz9k29ac4x0xeez.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhahrhgz9k29ac4x0xeez.png" alt="Final diagram" width="800" height="784"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For now, D2 has limited capability of styling, so we are not able to fully reproduce the original diagram. However, we just wrote an easily reproducible and maintainable diagram in less than 70 LOC!&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;I hope that this post has convinced you that Diagrams as Code fit the modern's software architect's tech stack perfectly. &lt;br&gt;
They are easy to write, easy to maintain, and easy to share. Give them a go when you have the chance!&lt;/p&gt;

</description>
      <category>d2</category>
      <category>diagrams</category>
    </item>
  </channel>
</rss>
