I love Django—it covers pretty much everything I need in a web environment. However, when it comes time to deploy a project to production, there are always a bunch of pre-deployment checks, and I can never seem to remember them all. I find myself constantly revisiting the official Django deployment checklist page.
Today I realized I don’t need all the detailed information every time—just a simple reminder list is enough. So, I built an interactive Django deployment checklist using Great Tables in marimo and hosted it on marimo.app . Now I can interact with it whenever I need a quick double-check.
marimo
The widgets may take a few moments to load, as they rely on WebAssembly under the hood.
I asked AI to generate a checklist and wrapped it in a Polars DataFrame
called df
.
I created 10 switch widgets and stacked them into an array widget named status_widgets
to represent the status of each checklist item.
I extracted the HTML representation of each widget via its _repr_html_()
method and inserted it as a new "Status"
column in df
, which I then wrapped in a Great Tables GT object.
I added two source notes using GT.tab_source_note() —one to display progress, and another for a visual progress bar.
Finally, I gave the table a nice header with GT.tab_header() and applied some styling using GT.opt_stylize() .
import%20marimo%20as%20mo%0Aimport%20polars%20as%20pl%0Afrom%20great_tables%20import%20GT%2C%20html%2C%20md
tasks%20%3D%20%5B%0A%20%20%20%20%22Set%20DEBUG%20%3D%20False%22%2C%0A%20%20%20%20%22Configure%20ALLOWED_HOSTS%22%2C%0A%20%20%20%20%22Set%20up%20a%20secret%20key%22%2C%0A%20%20%20%20%22Collect%20static%20files%22%2C%0A%20%20%20%20%22Apply%20database%20migrations%22%2C%0A%20%20%20%20%22Set%20up%20gunicorn%20or%20uWSGI%22%2C%0A%20%20%20%20%22Configure%20reverse%20proxy%20(e.g.%2C%20Nginx)%22%2C%0A%20%20%20%20%22Secure%20the%20database%22%2C%0A%20%20%20%20%22Set%20up%20HTTPS%20(SSL)%22%2C%0A%20%20%20%20%22Configure%20logging%20%26%20monitoring%22%2C%0A%5D%0A%0Anotes%20%3D%20%5B%0A%20%20%20%20%22Never%20deploy%20with%20DEBUG%20%3D%20True%20%E2%9A%A0%EF%B8%8F%22%2C%0A%20%20%20%20%22Include%20your%20domain(s)%20or%20IP%20address%20%F0%9F%8C%90%22%2C%0A%20%20%20%20%22Use%20a%20strong%2C%20secure%20key%20from%20an%20environment%20variable%20%F0%9F%94%90%22%2C%0A%20%20%20%20%22Run%20%60python%20manage.py%20collectstatic%60%20%F0%9F%93%A6%22%2C%0A%20%20%20%20%22Run%20%60python%20manage.py%20migrate%60%20%F0%9F%97%83%EF%B8%8F%22%2C%0A%20%20%20%20%22Use%20as%20a%20WSGI%20server%20in%20production%20%F0%9F%94%84%22%2C%0A%20%20%20%20%22Serve%20static%2Fmedia%20files%20and%20forward%20to%20WSGI%20server%20%F0%9F%A7%AD%22%2C%0A%20%20%20%20%22Use%20strong%20credentials%2C%20disable%20remote%20root%20login%20%F0%9F%9B%A1%EF%B8%8F%22%2C%0A%20%20%20%20%22Use%20Let's%20Encrypt%20or%20your%20own%20certificate%20%F0%9F%94%92%22%2C%0A%20%20%20%20%22Track%20errors%20and%20app%20performance%20%F0%9F%93%8A%22%2C%0A%5D%0A%0An_row%20%3D%20len(tasks)%0Astatus%20%3D%20%5B%22%E2%98%90%22%5D%20*%20n_row%0Adata%20%3D%20%7B%22Status%22%3A%20status%2C%20%22Task%22%3A%20tasks%2C%20%22Notes%22%3A%20notes%7D%0A%0Adf%20%3D%20pl.DataFrame(data)
status_widget%20%3D%20mo.ui.switch()%0Astatus_widgets%20%3D%20mo.ui.array(%5Bstatus_widget%5D%20*%20n_row)
def%20create_bar(%0A%20%20%20%20x%3A%20float%2C%0A%20%20%20%20max_width%3A%20int%2C%0A%20%20%20%20height%3A%20int%2C%0A%20%20%20%20background_color1%3A%20str%2C%0A%20%20%20%20background_color2%3A%20str%2C%0A)%20-%3E%20str%3A%0A%20%20%20%20width%20%3D%20round(max_width%20*%20x%2C%202)%0A%20%20%20%20px_width%20%3D%20f%22%7Bwidth%7Dpx%22%0A%20%20%20%20return%20f%22%22%22%5C%0A%20%20%20%20%3Cdiv%20style%3D%22width%3A%20%7Bmax_width%7Dpx%3B%20background-color%3A%20%7Bbackground_color1%7D%3B%22%3E%5C%0A%20%20%20%20%20%20%20%20%3Cdiv%20style%3D%22height%3A%7Bheight%7Dpx%3Bwidth%3A%7Bpx_width%7D%3Bbackground-color%3A%7Bbackground_color2%7D%3B%22%3E%3C%2Fdiv%3E%5C%0A%20%20%20%20%3C%2Fdiv%3E%5C%0A%20%20%20%20%22%22%22
done_count%20%3D%20sum(s.value%20for%20s%20in%20status_widgets)%0A%0Agt%20%3D%20(%0A%20%20%20%20GT(%0A%20%20%20%20%20%20%20%20df.with_columns(%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.Series(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5Bstatus._repr_html_()%20for%20status%20in%20status_widgets%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20).alias(%22Status%22)%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%20%20%20%20.tab_source_note(f%22%7Bdone_count%7D%20%2F%20%7Bn_row%7D%22)%0A%20%20%20%20.tab_source_note(%0A%20%20%20%20%20%20%20%20html(%0A%20%20%20%20%20%20%20%20%20%20%20%20create_bar(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20done_count%20%2F%20n_row%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20max_width%3D750%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20height%3D20%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20background_color1%3D%22lightgray%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20background_color2%3D%22%2366CDAA%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%20%20%20%20.tab_header(%22%E2%9C%85%20Django%20Deployment%20Checklist%22)%0A%20%20%20%20.opt_stylize(color%3D%22cyan%22%2C%20style%3D4)%0A)%0Agt
Check out the full marimo code below or view it on molab .
Show full code
import marimo
__generated_with = "0.14.7"
app = marimo.App(width= "medium" )
@app.cell
def _():
import marimo as mo
return (mo,)
@app.cell
def _():
import polars as pl
from great_tables import GT, html, md
return GT, html, pl
@app.cell
def _(pl):
tasks = [
"Set DEBUG = False" ,
"Configure ALLOWED_HOSTS" ,
"Set up a secret key" ,
"Collect static files" ,
"Apply database migrations" ,
"Set up gunicorn or uWSGI" ,
"Configure reverse proxy (e.g., Nginx)" ,
"Secure the database" ,
"Set up HTTPS (SSL)" ,
"Configure logging & monitoring" ,
]
notes = [
"Never deploy with DEBUG = True ⚠️" ,
"Include your domain(s) or IP address 🌐" ,
"Use a strong, secure key from an environment variable 🔐" ,
"Run `python manage.py collectstatic` 📦" ,
"Run `python manage.py migrate` 🗃️" ,
"Use as a WSGI server in production 🔄" ,
"Serve static/media files and forward to WSGI server 🧭" ,
"Use strong credentials, disable remote root login 🛡️" ,
"Use Let's Encrypt or your own certificate 🔒" ,
"Track errors and app performance 📊" ,
]
n_row = len (tasks)
status = ["☐" ] * n_row
data = {"Status" : status, "Task" : tasks, "Notes" : notes}
df = pl.DataFrame(data)
return df, n_row
@app.cell
def _(mo, n_row):
status_widget = mo.ui.switch()
status_widgets = mo.ui.array([status_widget] * n_row)
return (status_widgets,)
@app.function
def create_bar(
x: float ,
max_width: int ,
height: int ,
background_color1: str ,
background_color2: str ,
) -> str :
width = round (max_width * x, 2 )
px_width = f" { width} px"
return f""" \
<div style="width: { max_width} px; background-color: { background_color1} ;"> \
<div style="height: { height} px;width: { px_width} ;background-color: { background_color2} ;"></div> \
</div> \
"""
@app.cell
def _(GT, df, html, n_row, pl, status_widgets):
done_count = sum (s.value for s in status_widgets)
gt = (
GT(
df.with_columns(
pl.Series(
[status._repr_html_() for status in status_widgets]
).alias("Status" )
)
)
.tab_source_note(f" { done_count} / { n_row} " )
.tab_source_note(
html(
create_bar(
done_count / n_row,
max_width= 750 ,
height= 20 ,
background_color1= "lightgray" ,
background_color2= "#66CDAA" ,
)
)
)
.tab_header("✅ Django Deployment Checklist" )
.opt_stylize(color= "cyan" , style= 4 )
)
gt
return
if __name__ == "__main__" :
app.run()
This table is for demonstration purposes only. You should customize it based on your own needs.
This post was drafted by me, with AI assistance to refine the content.