<?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: Văn Hiếu Lê</title>
    <description>The latest articles on DEV Community by Văn Hiếu Lê (@heterl0).</description>
    <link>https://dev.to/heterl0</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%2F2248604%2Fa7a5f4c9-a927-41b9-b390-0753dd9d50a8.jpg</url>
      <title>DEV Community: Văn Hiếu Lê</title>
      <link>https://dev.to/heterl0</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/heterl0"/>
    <language>en</language>
    <item>
      <title>Dockerizing a Django Backend with Multi-Container Images: A Step-by-Step Guide</title>
      <dc:creator>Văn Hiếu Lê</dc:creator>
      <pubDate>Mon, 19 May 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/heterl0/dockerizing-a-django-backend-with-multi-container-images-a-step-by-step-guide-2mci</link>
      <guid>https://dev.to/heterl0/dockerizing-a-django-backend-with-multi-container-images-a-step-by-step-guide-2mci</guid>
      <description>&lt;p&gt;Managing a Django backend in production can be tricky—especially when you need to run a database, background tasks, caching system, and a web server together. Docker and Docker Compose make this easier. They let you define and run all these services in containers that work together.&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%2Fiuethfkduo6mjs6j6nsb.webp" 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%2Fiuethfkduo6mjs6j6nsb.webp" alt="Dockerize your Django backend using a multi-container setup with MySQL, Redis, Celery, and Nginx." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this guide, I’ll walk you through how to containerize a Django project using Docker and Docker Compose with multiple services like MySQL, Redis, Celery, and Nginx.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Outline&lt;/strong&gt;
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Introduction&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Project Structure Overview&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Writing the Dockerfile&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Composing Services with &lt;code&gt;docker-compose.yaml&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Service Breakdown&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Environment Variables and Secrets&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Development and Production Tips&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Running and Testing the Stack&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Conclusion&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;References&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;1. Introduction&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;When running Django in production, you need to think about dependencies, performance, and security. Docker helps by putting everything your app needs into containers. Docker Compose allows you to run several containers at the same time—like your Django app, database, task queue, and web server.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;2. Project Structure Overview&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Here’s what the project folder looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
├── Dockerfile
├── docker-compose.yaml
├── Pipfile
├── Pipfile.lock
├── nginx/
│ └── conf.d/
├── staticfiles/
├── your_project/
│ └── wsgi.py
└── ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dockerfile&lt;/strong&gt; – Builds the image for the Django app.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;docker-compose.yaml&lt;/strong&gt; – Defines and manages all containers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;nginx/conf.d&lt;/strong&gt; – Stores Nginx settings.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;staticfiles/&lt;/strong&gt; – Contains Django static files after running &lt;code&gt;collectstatic&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;3. Writing the Dockerfile&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This &lt;code&gt;Dockerfile&lt;/code&gt; creates a container for your Django project:&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;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.10-slim&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;RUN &lt;/span&gt;apt-get update &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;    default-libmysqlclient-dev &lt;span class="se"&gt;\
&lt;/span&gt;    pkg-config &lt;span class="se"&gt;\
&lt;/span&gt;    curl &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get clean &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;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; pipenv

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; Pipfile Pipfile.lock ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pipenv &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--deploy&lt;/span&gt; &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pip uninstall &lt;span class="nt"&gt;-y&lt;/span&gt; pipenv virtualenv-clone virtualenv

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&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;CMD&lt;/span&gt;&lt;span class="s"&gt; ["gunicorn", "--bind", "0.0.0.0:8000", "your_project.wsgi:application"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Main steps explained:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Uses a small Python image.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Installs tools and libraries for Django and MySQL.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Installs Python packages using Pipenv.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Copies project files into the container.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Collects static files for Nginx to serve.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Starts the app using Gunicorn, a reliable web server for Python apps.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;4. Composing Services with&lt;/strong&gt; &lt;code&gt;**docker-compose.yaml**&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;docker-compose.yaml&lt;/code&gt; file brings all the services together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version: "3.8"

services:
  db:
    image: mysql:8.0
    restart: always
    volumes:
      - db_data:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - MYSQL_DATABASE=${MYSQL_DATABASE}
      - MYSQL_USER=${MYSQL_USER}
      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
    command: --default-authentication-plugin=mysql_native_password
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  web:
    build: .
    restart: always
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    environment:
      - DATABASE_URL=mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@db:3306/${MYSQL_DATABASE}
      - REDIS_URL=redis://redis:6379/0
      - DEBUG=${DEBUG:-True}
      - SECRET_KEY=${SECRET_KEY}
      - ALLOWED_HOSTS=${ALLOWED_HOSTS:-localhost,127.0.0.1}
    volumes:
      - ./staticfiles:/app/staticfiles
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health/"]
      interval: 30s
      timeout: 10s
      retries: 3

  celery:
    build: .
    command: celery -A your_project worker -l INFO
    restart: always
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    environment:
      - DATABASE_URL=mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@db:3306/${MYSQL_DATABASE}
      - REDIS_URL=redis://redis:6379/0
      - DEBUG=${DEBUG:-True}
      - SECRET_KEY=${SECRET_KEY}
    volumes:
      - ./staticfiles:/app/staticfiles

  celery-beat:
    build: .
    command: celery -A your_project beat -l INFO
    restart: always
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    environment:
      - DATABASE_URL=mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@db:3306/${MYSQL_DATABASE}
      - REDIS_URL=redis://redis:6379/0
      - DEBUG=${DEBUG:-True}
      - SECRET_KEY=${SECRET_KEY}
    volumes:
      - ./staticfiles:/app/staticfiles

  redis:
    image: redis:7
    restart: always
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  nginx:
    image: nginx:1.23
    restart: always
    ports:
      - "${NGINX_HTTP_PORT:-80}:80"
      - "${NGINX_HTTPS_PORT:-443}:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./staticfiles:/var/www/html/static
      - /etc/letsencrypt:/etc/letsencrypt:ro
    depends_on:
      web:
        condition: service_healthy

volumes:
  db_data:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;db&lt;/strong&gt; – MySQL database&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;web&lt;/strong&gt; – Django app&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;celery&lt;/strong&gt; – Celery worker for background jobs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;celery-beat&lt;/strong&gt; – Schedules recurring tasks&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;redis&lt;/strong&gt; – Used for caching and as a Celery broker&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;nginx&lt;/strong&gt; – Serves static files and acts as a reverse proxy&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;5. Service Breakdown&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Database (MySQL)&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Uses the &lt;code&gt;mysql:8.0&lt;/code&gt; image.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Data is stored in a Docker volume so it won’t be lost when containers stop.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Environment variables are used to set username, password, and database name.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A healthcheck waits until MySQL is ready before other services start.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Django Web App&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Built from the Dockerfile.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Depends on &lt;code&gt;db&lt;/code&gt; and &lt;code&gt;redis&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Uses environment variables for setup (like DB connection and Redis).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mounts the &lt;code&gt;staticfiles&lt;/code&gt; folder for serving assets.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A healthcheck checks the &lt;code&gt;/health/&lt;/code&gt; endpoint.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Celery &amp;amp; Celery Beat&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Both use the same image as the Django app.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;celery&lt;/code&gt; handles background tasks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;celery-beat&lt;/code&gt; schedules repeating jobs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Both need the database and Redis.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Share the same environment setup.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Redis&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Uses the official &lt;code&gt;redis:7&lt;/code&gt; image.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Acts as an in-memory database and message broker.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Healthcheck makes sure Redis is running properly.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Nginx&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Works as a reverse proxy and static file server.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Forwards requests to Django and serves static assets.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Starts only after the Django app passes its healthcheck.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;6. Environment Variables and Secrets&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Use environment variables to keep sensitive info out of your code. Store them in a &lt;code&gt;.env&lt;/code&gt; file and load them in &lt;code&gt;docker-compose.yaml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Example variables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;MYSQL_USER&lt;/code&gt;, &lt;code&gt;MYSQL_PASSWORD&lt;/code&gt;, &lt;code&gt;MYSQL_DATABASE&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;DATABASE_URL&lt;/code&gt;, &lt;code&gt;REDIS_URL&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;DEBUG&lt;/code&gt;, &lt;code&gt;SECRET_KEY&lt;/code&gt;, &lt;code&gt;ALLOWED_HOSTS&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Never commit your &lt;code&gt;.env&lt;/code&gt; file to version control (like GitHub).&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;7. Development and Production Tips&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;In &lt;strong&gt;development&lt;/strong&gt; , mount your project as a volume to auto-reload code, and set &lt;code&gt;DEBUG=True&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In &lt;strong&gt;production&lt;/strong&gt; , set &lt;code&gt;DEBUG=False&lt;/code&gt; and use a secure &lt;code&gt;SECRET_KEY&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run &lt;code&gt;collectstatic&lt;/code&gt; during the Docker build so Nginx can serve static files.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run migrations after all services are up.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;8. Running and Testing the Stack&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;Build and Start:&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. &lt;strong&gt;Run Migrations:&lt;/strong&gt;
&lt;/h3&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;web python manage.py migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. &lt;strong&gt;Open Your App:&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Visit &lt;code&gt;http://localhost&lt;/code&gt; in your browser.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Static files are served by Nginx.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Celery and Celery Beat run in the background.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. &lt;strong&gt;Check Status:&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each container should show a healthy status if running correctly.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;9. Conclusion&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Containerizing a Django backend with Docker makes it easier to build, test, and deploy your app. By breaking your system into services—like the database, task queue, and web server—you can manage and scale each part independently.&lt;/p&gt;

&lt;p&gt;Using Docker Compose, you can spin up the whole stack with a single command. This setup not only saves time but also makes your deployment process more stable and secure.&lt;/p&gt;

&lt;p&gt;If you're planning to move your Django app to production, this multi-container setup is a smart step forward. Happy coding!&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;10. References&lt;/strong&gt;
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://www.docker.com/blog/how-to-dockerize-django-app/" rel="noopener noreferrer"&gt;Dockerize a Django App: Step-by-Step Guide for Beginners | Docker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://viblo.asia/p/su-dung-docker-va-ca-docker-compose-cho-du-an-django-AQrMJbWNM40E" rel="noopener noreferrer"&gt;Sử dụng Docker (và cả Docker Compose) cho dự án Django&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/@kenkono/docker-for-django-nginx-and-mysql-5960a611829e" rel="noopener noreferrer"&gt;Docker for Django(Nginx and MySQL) | by Ken Kono | Medium&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>docker</category>
      <category>experience</category>
      <category>guide</category>
      <category>django</category>
    </item>
    <item>
      <title>Moving My Dockerized Backend from Azure VPS to a Cheaper VPS</title>
      <dc:creator>Văn Hiếu Lê</dc:creator>
      <pubDate>Mon, 12 May 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/heterl0/moving-my-dockerized-backend-from-azure-vps-to-a-cheaper-vps-4co4</link>
      <guid>https://dev.to/heterl0/moving-my-dockerized-backend-from-azure-vps-to-a-cheaper-vps-4co4</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;After running my backend server on an Azure Student VPS for about three months with a $100 budget, I started looking for a cheaper, long-term solution. My goal was to find a VPS that costs around &lt;strong&gt;$5–$10 per month&lt;/strong&gt; and is easy to maintain for small projects.&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%2Fi9uya0wt198tjjxdkcjh.webp" 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%2Fi9uya0wt198tjjxdkcjh.webp" alt="Moving My Dockerized Backend from Azure VPS to a Cheaper VPS" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here’s how I migrated my &lt;strong&gt;Dockerized Django backend&lt;/strong&gt; from Azure to another VPS, and what I learned from the experience.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Azure VPS Specs
&lt;/h2&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%2F4tpc6glfhltmqkqej0xr.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%2F4tpc6glfhltmqkqej0xr.png" alt="Neofetch my Azure vps" width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I used Azure's student subscription for three months. Here were the server specs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;1 vCPU&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2 GB RAM&lt;/strong&gt; (no swap)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;32 GB SSD&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;OS: Ubuntu Server&lt;/li&gt;
&lt;li&gt;Services: MySQL, Nginx, Redis, Python (Pipenv), Git&lt;/li&gt;
&lt;/ul&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%2F5996g601mo5innoq4g7d.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%2F5996g601mo5innoq4g7d.png" alt="This show how much ram for my backend is running" width="800" height="409"&gt;&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;htop
&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%2Fnrlt5luw7rv13mxsmxed.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%2Fnrlt5luw7rv13mxsmxed.png" alt="Here show that how storage my project is using" width="800" height="293"&gt;&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;df -H
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I found that this configuration worked well for my small backend. So, I looked for a similar VPS spec in a more affordable price range.&lt;/p&gt;




&lt;h2&gt;
  
  
  Researching Affordable VPS Options
&lt;/h2&gt;

&lt;p&gt;I checked several providers and compared their plans:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Price (USD)&lt;/th&gt;
&lt;th&gt;Price (VND)&lt;/th&gt;
&lt;th&gt;Specs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vultr&lt;/td&gt;
&lt;td&gt;$10.00&lt;/td&gt;
&lt;td&gt;249,300 VND&lt;/td&gt;
&lt;td&gt;1 vCPU, 2 GB RAM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CloudCone&lt;/td&gt;
&lt;td&gt;$5.78&lt;/td&gt;
&lt;td&gt;144,095 VND&lt;/td&gt;
&lt;td&gt;1 vCPU, 2 GB RAM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaleway&lt;/td&gt;
&lt;td&gt;$3.41&lt;/td&gt;
&lt;td&gt;85,011 VND&lt;/td&gt;
&lt;td&gt;1 vCPU, 2 GB RAM (pre-tax)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OVH&lt;/td&gt;
&lt;td&gt;$0.97&lt;/td&gt;
&lt;td&gt;24,182 VND&lt;/td&gt;
&lt;td&gt;(Very basic specs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hetzner&lt;/td&gt;
&lt;td&gt;$4.79&lt;/td&gt;
&lt;td&gt;119,415 VND&lt;/td&gt;
&lt;td&gt;1 vCPU, 2 GB RAM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;_That is the result was returned by Perplexity, and I do not confirm the research yet! _&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I also looked at &lt;strong&gt;VinaHost&lt;/strong&gt; , a local Vietnamese provider. Their VPS offers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;3 vCPU&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;3 GB RAM&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cost: &lt;strong&gt;250,000 VND/month&lt;/strong&gt; (pre-tax)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;50% discount for 6–12 month subscriptions&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With the discount, I paid &lt;strong&gt;&lt;del&gt;125,000 VND/month (&lt;/del&gt;$5.10 per month)&lt;/strong&gt; for 6 months. This plan gave me more resources at a cheaper rate — within my budget and flexible enough to adapt to future growth or changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Dockerized My Backend
&lt;/h2&gt;

&lt;p&gt;Each time I moved my backend before, I had to manually reinstall and configure everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Git&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;MySQL&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Python + Pipenv&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Nginx&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Redis&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SSL with Let's Encrypt&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It was &lt;strong&gt;time-consuming and error-prone&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So, I decided to &lt;strong&gt;Dockerize my Django backend&lt;/strong&gt;. I had some experience using Docker from my company’s projects (NestJS backend), where we used &lt;code&gt;docker compose&lt;/code&gt; to run dev and prod environments easily.&lt;/p&gt;

&lt;p&gt;I also learned that some &lt;strong&gt;frontend job interviews&lt;/strong&gt; ask about Docker — so it’s a good skill to have.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I plan to write another blog post specifically on &lt;strong&gt;&lt;a href="https://heterl0.live/blog/dockerizing-a-django-backend-with-multi-container-images/" rel="noopener noreferrer"&gt;Dockerizing a Django backend&lt;/a&gt;&lt;/strong&gt; soon.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Migration Steps
&lt;/h2&gt;

&lt;p&gt;I documented my process for migrating the backend from Azure to the new VPS.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✅ On the Old VPS
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Backup the MySQL database&lt;/strong&gt; to a &lt;code&gt;.sql&lt;/code&gt; file.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Backup Let's Encrypt&lt;/strong&gt; certificates.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Save the Nginx configuration&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Write &lt;code&gt;Dockerfile&lt;/code&gt; and &lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test the Docker setup&lt;/strong&gt; on my local machine using WSL on Windows.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  🚚 On the New VPS
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Bought the new VPS&lt;/strong&gt; from VinaHost.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Set up GitHub SSH&lt;/strong&gt; to clone my project repo.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Set up MySQL container&lt;/strong&gt; and created the database and user.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Imported the &lt;code&gt;.sql&lt;/code&gt; backup&lt;/strong&gt; into the database container.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ran &lt;code&gt;docker-compose up -d&lt;/code&gt;&lt;/strong&gt; to start the app stack.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Handled Nginx conflict:&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pointed the domain DNS&lt;/strong&gt; to the new VPS IP.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Restored SSL certificates&lt;/strong&gt; from the backup.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Boom — my backend was live again!&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This was my first time using Docker to migrate a server, and it made the process much easier and faster.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;No more manually setting up services&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Easy to test locally with &lt;code&gt;docker compose&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;More flexibility to scale or move in the future&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks to &lt;strong&gt;VinaHost’s 50% discount&lt;/strong&gt; , I got a &lt;strong&gt;3GB RAM / 3vCPU VPS for just ~$5.10/month&lt;/strong&gt; , well within my budget.&lt;/p&gt;




&lt;h2&gt;
  
  
  Feedback Welcome!
&lt;/h2&gt;

&lt;p&gt;I’m still new to Docker, so if you notice any mistakes or have suggestions, feel free to leave a comment. I'd love to learn and improve my setup.&lt;/p&gt;

&lt;p&gt;Thanks for reading! 🚀&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>experience</category>
    </item>
    <item>
      <title>Set up Celery production for Django project</title>
      <dc:creator>Văn Hiếu Lê</dc:creator>
      <pubDate>Sat, 05 Apr 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/heterl0/set-up-celery-production-for-django-project-25fc</link>
      <guid>https://dev.to/heterl0/set-up-celery-production-for-django-project-25fc</guid>
      <description>&lt;h2&gt;
  
  
  🚀 Background
&lt;/h2&gt;

&lt;p&gt;Celery is a &lt;strong&gt;cron job service&lt;/strong&gt; commonly used in Django projects. In my case, I used Celery to &lt;strong&gt;reset user streaks at midnight (00:00 AM)&lt;/strong&gt; for my application.&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%2Fheterl0.is-a.dev%2Fblog%2Fset-up-celery-for-django-project%2Fset-up-celery-for-django-project.webp" 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%2Fheterl0.is-a.dev%2Fblog%2Fset-up-celery-for-django-project%2Fset-up-celery-for-django-project.webp" alt="Set up the Celery for django project" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In development, I had to run three terminal windows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;One for the Django server: &lt;code&gt;python manage.py runserver&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One for the Celery worker: &lt;code&gt;celery -A proj worker&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One for the Celery beat: &lt;code&gt;celery -A proj beat&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That setup was a bit complex. So in this blog, I’ll share &lt;strong&gt;how I set up Celery as a production-ready service&lt;/strong&gt; using &lt;code&gt;systemd&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠️ Approach
&lt;/h2&gt;

&lt;p&gt;When I first moved my app to production, I used &lt;strong&gt;Gunicorn&lt;/strong&gt; to serve Django, but forgot about Celery. As a result, scheduled tasks didn’t run because &lt;strong&gt;both Celery Worker and Celery Beat need to run in parallel&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✅ Option 1: Using tmux
&lt;/h3&gt;

&lt;p&gt;Initially, I used &lt;code&gt;tmux&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;SSH into the server.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Start a &lt;code&gt;tmux&lt;/code&gt; session and split the window with &lt;code&gt;Ctrl + b&lt;/code&gt; → &lt;code&gt;%&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run the worker and beat processes in separate panes.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Even after closing SSH, the processes stayed alive (confirmed using &lt;code&gt;htop&lt;/code&gt;). This works, but it’s not ideal for long-term use.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✅ Option 2: Using systemd Services
&lt;/h3&gt;

&lt;p&gt;When I got a new VPS, I wanted a better solution. After some research (and help from ChatGPT 😄), I found a reliable approach using &lt;code&gt;systemd&lt;/code&gt; services to run Celery in the background.&lt;/p&gt;

&lt;p&gt;Reference: &lt;a href="https://dev.to/iamtekson/django-with-celery-in-production-cb5"&gt;Django with Celery in Production&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚙️ 1. Create the Celery Worker Service
&lt;/h2&gt;

&lt;p&gt;Create a new service file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo nano /etc/systemd/system/celery.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste the following (replace &lt;code&gt;user&lt;/code&gt;, paths, and &lt;code&gt;[celery_app]&lt;/code&gt; accordingly):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Unit]
Description=Celery Worker Service
After=network.target

[Service]
Type=simple
User=your_username
Group=your_username
WorkingDirectory=/home/your_username/your_project
ExecStart=/home/your_username/.local/share/virtualenvs/your-venv/bin/celery -A [celery_app] worker --loglevel=INFO
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👉 Replace:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;your_username&lt;/code&gt; with your actual Linux username.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;[celery_app]&lt;/code&gt; with the value from your &lt;code&gt;celery.py&lt;/code&gt; file.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl daemon-reload
sudo systemctl enable celery
sudo systemctl start celery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🕒 2. Create the Celery Beat Service
&lt;/h2&gt;

&lt;p&gt;Now create the Beat service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo nano /etc/systemd/system/celery-beat.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Unit]
Description=Celery Beat Service
After=network.target

[Service]
Type=simple
User=your_username
Group=your_username
WorkingDirectory=/home/your_username/your_project
ExecStart=/home/your_username/.local/share/virtualenvs/your-venv/bin/celery -A [celery_app] beat --loglevel=INFO
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl daemon-reload
sudo systemctl enable celery-beat
sudo systemctl start celery-beat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✅ Check status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl status celery-beat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see &lt;code&gt;active (running)&lt;/code&gt;, it means everything is set up correctly.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧾 Conclusion
&lt;/h2&gt;

&lt;p&gt;That’s how I set up Celery and Celery Beat in production using &lt;code&gt;systemd&lt;/code&gt;. It’s a clean, reliable, and maintainable way to manage background tasks in Django.&lt;/p&gt;

&lt;p&gt;📚 References:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/iamtekson/django-with-celery-in-production-cb5"&gt;Post by Tek Kshetri&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.celeryq.dev/en/stable/getting-started/introduction.html" rel="noopener noreferrer"&gt;Celery Documentation&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for reading! 🙏 Hope this helps you in your deployment journey.&lt;/p&gt;

</description>
      <category>experience</category>
      <category>celery</category>
      <category>django</category>
      <category>production</category>
    </item>
    <item>
      <title>Practice Touch Typing with MonkeyType</title>
      <dc:creator>Văn Hiếu Lê</dc:creator>
      <pubDate>Sun, 16 Mar 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/heterl0/practice-touch-typing-with-monkeytype-19j1</link>
      <guid>https://dev.to/heterl0/practice-touch-typing-with-monkeytype-19j1</guid>
      <description>&lt;h2&gt;
  
  
  🚀 Update Process
&lt;/h2&gt;

&lt;p&gt;Today is March 16, 2025—approximately one month since &lt;a href="https://heterl0.is-a.dev/blog/touch-typing-practice-feb-2025" rel="noopener noreferrer"&gt;my last update&lt;/a&gt;. Initially, I didn't notice significant improvements in my typing skills, so I decided to increase my daily practice time from 10 minutes to 25 minutes. This adjustment helped accelerate my progress and resulted in new personal records:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Average Typing Speed:&lt;/strong&gt; Improved to around &lt;strong&gt;88–90 WPM&lt;/strong&gt; for the 15-second tests.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;New Personal Best (15s test):&lt;/strong&gt; Achieved a record of &lt;strong&gt;116 WPM&lt;/strong&gt; 🎉.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;New Personal Best (10-word test):&lt;/strong&gt; Reached an impressive &lt;strong&gt;143 WPM&lt;/strong&gt; 🎯.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&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%2Fyth7by74t80o3ps3pjg1.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%2Fyth7by74t80o3ps3pjg1.png" alt="Monkeytype profile of heterl0 | March 2025" width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These results are encouraging and validate the effectiveness of increasing my daily practice time.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 My Approach
&lt;/h2&gt;

&lt;h2&gt;
  
  
  ⏳ Increasing Daily Typing Time
&lt;/h2&gt;

&lt;p&gt;By practicing 25 minutes daily, I accumulate approximately &lt;strong&gt;750 minutes (12.5 hours)&lt;/strong&gt; per month. Although this increase is beneficial, it's difficult to precisely measure its impact due to various influencing factors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;My mood during practice sessions 😊&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fatigue levels after work 💤&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Physical comfort and hand condition ✋&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because these factors vary daily, it's challenging to pinpoint exactly how much each contributes to my performance. Thus, I've decided to rely on the average WPM as a reliable indicator of improvement.&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%2Ft0ob5446jpmziv50cwg3.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%2Ft0ob5446jpmziv50cwg3.png" alt="Normal distribution of few days" width="800" height="252"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I trust in the principle of Normal Distribution 📊—most of my typing tests consistently fall within the &lt;strong&gt;80–90 WPM&lt;/strong&gt; range, indicating that my true average is approximately &lt;strong&gt;88 WPM&lt;/strong&gt;. This measurement feels accurate and reasonable.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠️ Using Monkeytype Logger Error Extension
&lt;/h2&gt;

&lt;p&gt;To further refine my typing accuracy, I created a custom browser extension called &lt;a href="https://heterl0.is-a.dev/blog/monkeytype-extension-logger-error" rel="noopener noreferrer"&gt;&lt;strong&gt;Monkeytype Logger Error&lt;/strong&gt;&lt;/a&gt;, which tracks and analyzes typing errors. This tool helps me identify problematic characters and words that frequently cause mistakes.&lt;/p&gt;

&lt;p&gt;Here's how you can use it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Install the extension from the &lt;a href="https://microsoftedge.microsoft.com/addons/detail/monkeytype-history-logger/ophgnpohledibffckhpabdcciniinnjo" rel="noopener noreferrer"&gt;Microsoft Store&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;On &lt;a href="https://monkeytype.com/" rel="noopener noreferrer"&gt;Monkeytype.com&lt;/a&gt;, navigate to Settings:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Start typing! Your errors will be logged automatically.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;After your session, export your error data by clicking the &lt;code&gt;Download JSON&lt;/code&gt; button.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Next, visit the &lt;a href="https://monkeytype-analysis.heterl0.live/" rel="noopener noreferrer"&gt;MonkeyType Analysis&lt;/a&gt; website and import your JSON file. The website will analyze your data and highlight frequent mistakes.&lt;/p&gt;

&lt;p&gt;I then use ChatGPT to generate customized typing exercises based on these common errors, helping me target specific weaknesses more effectively.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Important:&lt;/strong&gt; Remember to turn off &lt;code&gt;Always show words history&lt;/code&gt; when practicing custom texts to avoid data loops in your error logs.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🔄 Alternating Between 15s and 60s Tests
&lt;/h2&gt;

&lt;p&gt;Recently, I adopted a new training strategy: alternating daily between short (15-second) and longer (60-second) typing tests. Each test type offers unique benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;15-second tests:&lt;/strong&gt; Boost initial speed bursts ⚡️, improve accuracy early on, and reduce immediate errors.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;60-second tests:&lt;/strong&gt; Enhance typing endurance 🏃‍♂️, stability, and consistency—skills crucial for real-world tasks like coding or writing documentation.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I discovered that these two test types complement each other perfectly, creating a balanced improvement in both speed and endurance.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎖️ Conclusion &amp;amp; Next Steps
&lt;/h2&gt;

&lt;p&gt;This month's adjustments yielded noticeable improvements and new personal records—small victories that motivate me to continue this enjoyable journey toward mastering typing skills. My ultimate goal remains: achieving an average overall speed of &lt;strong&gt;100 WPM&lt;/strong&gt; 🚩.&lt;/p&gt;

&lt;p&gt;Tracking progress through these blogs makes this journey fun and meaningful for me—not just for practical purposes but as a personal challenge. Stay tuned for future updates!&lt;/p&gt;

&lt;p&gt;Feel free to explore more posts on my blog at &lt;a href="https://heterl0.live/" rel="noopener noreferrer"&gt;heterl0.live&lt;/a&gt;, follow my projects on &lt;a href="https://github.com/heterl0" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, or subscribe directly via &lt;a href="https://heterl0.live/feed/feed.xml" rel="noopener noreferrer"&gt;RSS Feed&lt;/a&gt;. Happy typing! 🌟&lt;/p&gt;

</description>
      <category>typing</category>
      <category>extension</category>
      <category>tips</category>
      <category>tricks</category>
    </item>
    <item>
      <title>Building Multi-Tenant Applications with Next.js: A Custom Subdomain Approach</title>
      <dc:creator>Văn Hiếu Lê</dc:creator>
      <pubDate>Tue, 11 Mar 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/heterl0/building-multi-tenant-applications-with-nextjs-a-custom-subdomain-approach-5105</link>
      <guid>https://dev.to/heterl0/building-multi-tenant-applications-with-nextjs-a-custom-subdomain-approach-5105</guid>
      <description>&lt;h2&gt;
  
  
  What is Multi-Tenant Architecture?
&lt;/h2&gt;

&lt;p&gt;Multi-tenancy is a software architecture where a single instance of an application serves multiple tenants (customers or users). Each tenant's data is logically isolated, ensuring privacy and security while sharing the same infrastructure, such as servers, databases, or applications. This model is widely used in SaaS platforms like Salesforce, Google Workspace, and Dropbox.&lt;/p&gt;

&lt;p&gt;Key features of multi-tenancy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shared Infrastructure&lt;/strong&gt; : Tenants share the same hardware and software resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tenant Isolation&lt;/strong&gt; : Data and configurations are isolated for each tenant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost Efficiency&lt;/strong&gt; : Shared resources reduce costs compared to single-tenant setups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt; : Easily accommodates more tenants by leveraging shared resources.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Building a Multi-Tenant Application with Next.js
&lt;/h2&gt;

&lt;p&gt;Checkout &lt;a href="https://login.heterl0.live/" rel="noopener noreferrer"&gt;Demo&lt;/a&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%2Fgid6o0jidh2qfpt3hgtf.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%2Fgid6o0jidh2qfpt3hgtf.png" alt="NextJs Multi Tenant Demo" width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This section outlines how to create a multi-tenant application using Next.js with subdomain-based tenant separation (e.g., &lt;code&gt;tenant1.yourdomain.com&lt;/code&gt;, &lt;code&gt;tenant2.yourdomain.com&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Folder Structure
&lt;/h3&gt;

&lt;p&gt;Organize your Next.js &lt;a href="https://github.com/heterl0/nextjs-multi-tenant-with-auth" rel="noopener noreferrer"&gt;project&lt;/a&gt; as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| 
├── app/
| ├── [subdomain]/ # Tenant-specific subdirectory
| │ ├── page.tsx # Main page for the tenant
| │ ├── layout.tsx # Layout for tenant pages
| └── middleware.ts # Middleware for request handling
| public/
| └── images/tenant1/ # Tenant-specific static assets
| package.json # Project configuration
| .env # Environment variables
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Middleware for Subdomain Handling
&lt;/h3&gt;

&lt;p&gt;The middleware handles routing, authentication, and request rewriting based on subdomains. Below is the &lt;code&gt;middleware.ts&lt;/code&gt; code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/env&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;host&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tenantSlug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&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="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`http://login.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tenantSlug&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;login&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/login&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&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="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// In this example for easy auth I use userId as cookie auth Token&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;userId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;tenantSlug&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;login&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`http://login.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tenantSlug&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rewrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matcher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/((?!_next/static|_next/image|favicon.ico).*)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Subdomain Detection&lt;/strong&gt; :

&lt;ul&gt;
&lt;li&gt;The middleware extracts the subdomain from the &lt;code&gt;host&lt;/code&gt; header to identify the tenant.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt; :

&lt;ul&gt;
&lt;li&gt;If no &lt;code&gt;userId&lt;/code&gt; cookie exists, users are redirected to the login subdomain (&lt;code&gt;login.yourdomain.com&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route Rewriting&lt;/strong&gt; :

&lt;ul&gt;
&lt;li&gt;Requests are rewritten to serve tenant-specific pages under &lt;code&gt;/[subdomain]&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cookie Management&lt;/strong&gt; :

&lt;ul&gt;
&lt;li&gt;The login flow sets a &lt;code&gt;userId&lt;/code&gt; cookie upon successful authentication.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Steps to Implement
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Domain Configuration&lt;/strong&gt; :

&lt;ul&gt;
&lt;li&gt;Point your main domain and all subdomains to the same server.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment Variables&lt;/strong&gt; :

&lt;ul&gt;
&lt;li&gt;Define your root domain in an &lt;code&gt;.env&lt;/code&gt; file (e.g., &lt;code&gt;DOMAIN=yourdomain.com&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local Development&lt;/strong&gt; :

&lt;ul&gt;
&lt;li&gt;Use host aliases or local DNS tools to test subdomains locally (e.g., &lt;code&gt;127.0.0.1 login.localhost&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication Flow&lt;/strong&gt; :

&lt;ul&gt;
&lt;li&gt;Create a login page at &lt;code&gt;login.yourdomain.com&lt;/code&gt; that sets a cookie (&lt;code&gt;userId&lt;/code&gt;) upon user authentication.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; In the practice, we use &lt;code&gt;JWT&lt;/code&gt; or cookie instead. cross-domain can't maintain the cookie, so I pass it through by using query params.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tenant-Specific Pages&lt;/strong&gt; :

&lt;ul&gt;
&lt;li&gt;Build tenant-specific pages in the &lt;code&gt;[subdomain]&lt;/code&gt; directory under &lt;code&gt;app&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Advantages of This Approach
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt; : Easily add new tenants by creating new subdirectories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolation&lt;/strong&gt; : Each tenant has its own namespace and data separation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexibility&lt;/strong&gt; : Middleware allows dynamic routing and authentication handling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This setup ensures a robust multi-tenant architecture using Next.js while maintaining security and scalability through subdomain-based isolation and cookie-based authentication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclaimer
&lt;/h2&gt;

&lt;p&gt;The approach described in this blog post is based on my personal experience with a past project and may not be the most effective or efficient method for implementing multi-tenancy. While it draws inspiration from various sources, including &lt;a href="https://vercel.com/guides/nextjs-multi-tenant-application" rel="noopener noreferrer"&gt;How to Build a Multi-Tenant App with Custom Domains Using Next.js&lt;/a&gt;, it's important to note that this approach lacks robust authentication when users navigate to subdomains. For more comprehensive and up-to-date solutions, consider exploring other resources or official documentation. If you're interested in my other projects, you can check out my GitHub profile at &lt;a href="https://github.com/heterl0/" rel="noopener noreferrer"&gt;https://github.com/heterl0/&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://vercel.com/guides/nextjs-multi-tenant-application" rel="noopener noreferrer"&gt;How to Build a Multi-Tenant App with Custom Domains Using Next.js&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>nextjs</category>
      <category>authentication</category>
      <category>multitenant</category>
      <category>demo</category>
    </item>
    <item>
      <title>Building MonkeyType Logger: A Typing Tracker Error Extension</title>
      <dc:creator>Văn Hiếu Lê</dc:creator>
      <pubDate>Fri, 28 Feb 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/heterl0/building-monkeytype-logger-a-typing-tracker-error-extension-3p4i</link>
      <guid>https://dev.to/heterl0/building-monkeytype-logger-a-typing-tracker-error-extension-3p4i</guid>
      <description>&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://heterl0.is-a.dev/blog/monkeytype-extension-logger-error/#my-journey-to-faster-and-more-accurate-typing" rel="noopener noreferrer"&gt;My Journey to Faster and More Accurate Typing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://heterl0.is-a.dev/blog/monkeytype-extension-logger-error/#automating-the-error-tracking-process" rel="noopener noreferrer"&gt;Automating the Error Tracking Process&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://heterl0.is-a.dev/blog/monkeytype-extension-logger-error/#extension-for-tracking-typing-errors" rel="noopener noreferrer"&gt;Extension for Tracking Typing Errors&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://heterl0.is-a.dev/blog/monkeytype-extension-logger-error/#contentjs-capturing-typing-results" rel="noopener noreferrer"&gt;Content.js (Capturing Typing Results)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://heterl0.is-a.dev/blog/monkeytype-extension-logger-error/#backgroundjs-handling-events" rel="noopener noreferrer"&gt;background.js (Handling Events)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://heterl0.is-a.dev/blog/monkeytype-extension-logger-error/#popuphtml--popupjs-user-interface" rel="noopener noreferrer"&gt;popup.html &amp;amp; popup.js (User Interface)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://heterl0.is-a.dev/blog/monkeytype-extension-logger-error/#analyzing-typing-errors" rel="noopener noreferrer"&gt;Analyzing Typing Errors&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://heterl0.is-a.dev/blog/monkeytype-extension-logger-error/#jupyter-notebook-ai-based-analysis" rel="noopener noreferrer"&gt;Jupyter Notebook (AI-Based Analysis)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://heterl0.is-a.dev/blog/monkeytype-extension-logger-error/#website-for-data-visualization" rel="noopener noreferrer"&gt;Website for Data Visualization&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://heterl0.is-a.dev/blog/monkeytype-extension-logger-error/#conclusion" rel="noopener noreferrer"&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://heterl0.is-a.dev/blog/monkeytype-extension-logger-error/#check-out-my-work" rel="noopener noreferrer"&gt;Check Out My Work&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  My Journey to Faster and More Accurate Typing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Beginning
&lt;/h3&gt;

&lt;p&gt;Around &lt;strong&gt;four months ago&lt;/strong&gt; , I started practicing &lt;strong&gt;Monkeytype daily&lt;/strong&gt; to improve my &lt;strong&gt;typing speed and coding efficiency&lt;/strong&gt;. Initially, I made good progress, but after about &lt;strong&gt;one month&lt;/strong&gt; , I got &lt;a href="https://heterl0.is-a.dev/blog/touch-typing-practice-feb-2025" rel="noopener noreferrer"&gt;stuck at a speed of &lt;strong&gt;80-90 WPM&lt;/strong&gt;&lt;/a&gt; and couldn’t improve further.&lt;/p&gt;

&lt;p&gt;To break through this plateau, I decided to &lt;strong&gt;track my typing mistakes&lt;/strong&gt; systematically. I began &lt;strong&gt;writing down all the words I mistyped&lt;/strong&gt; and used &lt;strong&gt;AI to analyze them&lt;/strong&gt; , identifying patterns and areas for improvement. Additionally, I started practicing specific coding-related typing techniques to enhance accuracy.&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%2F8gd1zldez9md1cj97wps.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%2F8gd1zldez9md1cj97wps.png" alt="MonkeyType Track Error Logger" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Check out my extension here! &lt;a href="https://microsoftedge.microsoft.com/addons/detail/monkeytype-history-logger/ophgnpohledibffckhpabdcciniinnjo" rel="noopener noreferrer"&gt;Monkeytype History Logger - Microsoft Edge Addons&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating the Error Tracking Process
&lt;/h2&gt;

&lt;p&gt;I then thought: &lt;em&gt;Why not automate this tracking process?&lt;/em&gt; This led me to develop a &lt;strong&gt;browser extension&lt;/strong&gt; that &lt;strong&gt;automatically records error words&lt;/strong&gt; from my typing history.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Extension for Tracking Typing Errors&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Content.js&lt;/strong&gt; (Capturing Typing Results)
&lt;/h3&gt;

&lt;p&gt;This file detects the &lt;strong&gt;appearance of result history elements&lt;/strong&gt; on the screen and sends an event to &lt;code&gt;background.js&lt;/code&gt; for processing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "id": 1740714390907,
    "time": "2025-02-28T03:46:30.907Z",
    "words": [
      { "reason": "corrected", "word": "be" },
      { "reason": "corrected", "word": "part" },
      { "reason": "corrected", "word": "not" },
      { "reason": "error", "word": "then" },
      { "reason": "error", "word": "some" }
    ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each record consists of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;id&lt;/strong&gt; : A unique identifier for the session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;time&lt;/strong&gt; : The timestamp of the recorded session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;words&lt;/strong&gt; : A list of words where mistakes occurred.

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;reason&lt;/strong&gt; : Explanation for the mistake:&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;corrected&lt;/code&gt;: A word that was mistyped but corrected.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;error&lt;/code&gt;: A word that was mistyped and skipped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;word&lt;/strong&gt; : The actual word that caused the issue.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;background.js&lt;/strong&gt; (Handling Events)
&lt;/h3&gt;

&lt;p&gt;This script listens for events and processes them. It currently supports three key actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Saving Records&lt;/strong&gt; : The &lt;code&gt;SaveRecords&lt;/code&gt; event stores error logs in &lt;code&gt;chrome.storage.local&lt;/code&gt;. With the &lt;code&gt;unlimitedStorage&lt;/code&gt; permission, we can store up to &lt;strong&gt;10,000 records&lt;/strong&gt; (each 100 records take about &lt;strong&gt;50KB&lt;/strong&gt; ).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deleting Last Record&lt;/strong&gt; : The &lt;code&gt;DeleteLastRecords&lt;/code&gt; event removes the most recent entry for data management.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Downloading Records&lt;/strong&gt; : This event enables users to &lt;strong&gt;download their records as a JSON file&lt;/strong&gt; for offline analysis.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;popup.html &amp;amp; popup.js&lt;/strong&gt; (User Interface)
&lt;/h3&gt;

&lt;p&gt;The extension’s popup displays:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extension name&lt;/li&gt;
&lt;li&gt;Last recorded session&lt;/li&gt;
&lt;li&gt;Total stored records&lt;/li&gt;
&lt;li&gt;Buttons for &lt;strong&gt;download, delete, and import&lt;/strong&gt; functionalities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Current Issue&lt;/strong&gt; : Sometimes, &lt;code&gt;chrome.storage.local&lt;/code&gt; loses the records, resetting them to zero. As a workaround, I manually &lt;strong&gt;download and re-import&lt;/strong&gt; the data. Fixing this bug is a priority for the next update.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Analyzing Typing Errors&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Jupyter Notebook (AI-Based Analysis)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;If you're familiar with &lt;strong&gt;Python&lt;/strong&gt; and &lt;strong&gt;Jupyter Notebook&lt;/strong&gt; , you can analyze your typing data using AI techniques. Simply:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Place &lt;code&gt;monkeytype_data.json&lt;/code&gt; in the same directory as your Jupyter Notebook file.&lt;/li&gt;
&lt;li&gt;Run the notebook to see &lt;strong&gt;detailed insights&lt;/strong&gt; into your typing mistakes and improvements.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://github.com/heterl0/monkeytype-logger/blob/main/monkeytype-analysis/typing-error-analysis-notebook.ipynb" rel="noopener noreferrer"&gt;📊 View My Jupyter Notebook Analysis&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Website for Data Visualization&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;I also built a &lt;strong&gt;Next.js web app&lt;/strong&gt; (deployed on &lt;strong&gt;Vercel Hobby Tier&lt;/strong&gt; ) that lets users:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Upload their JSON file.&lt;/li&gt;
&lt;li&gt;Visualize typing mistakes and progress trends.&lt;/li&gt;
&lt;li&gt;Gain personalized insights for improvement.&lt;/li&gt;
&lt;/ul&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%2F29z580r6zc92ubed2v7w.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%2F29z580r6zc92ubed2v7w.png" alt="MonkeyType Analysis" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://monkeytype-analysis.heterl0.live/" rel="noopener noreferrer"&gt;🌐 Visit My Analysis Website&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This analysis helps me &lt;strong&gt;refine my typing habits&lt;/strong&gt; and develop an &lt;strong&gt;AI-based assistant&lt;/strong&gt; for further improvements.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Conclusion&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;If you want to improve your typing skills like I did, try this &lt;strong&gt;free and open-source extension&lt;/strong&gt;. Follow the instructions and start tracking your mistakes.&lt;/p&gt;

&lt;p&gt;💡 &lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real-time tracking of errors and corrections&lt;/li&gt;
&lt;li&gt;AI-powered analysis for targeted improvements&lt;/li&gt;
&lt;li&gt;Web-based visualization of your progress&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🚀 &lt;strong&gt;How to Get Started:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install the extension on your preferred browser.&lt;/li&gt;
&lt;li&gt;Practice typing and let the extension track mistakes automatically.&lt;/li&gt;
&lt;li&gt;Regularly review your errors and adjust your practice accordingly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Remember, &lt;strong&gt;consistent practice is key&lt;/strong&gt; to increasing your typing speed and accuracy. Happy typing! 🎯&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Check Out My Work&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;a href="https://github.com/heterl0/monkeytype-logger" rel="noopener noreferrer"&gt;My Extension on GitHub&lt;/a&gt;🔗 &lt;a href="https://microsoftedge.microsoft.com/addons/detail/monkeytype-history-logger/ophgnpohledibffckhpabdcciniinnjo" rel="noopener noreferrer"&gt;My Extension on Store&lt;/a&gt;🔗 &lt;a href="https://github.com/heterl0/monkeytype-logger/blob/main/monkeytype-analysis/typing-error-analysis-notebook.ipynb" rel="noopener noreferrer"&gt;My Jupyter Notebook Analysis&lt;/a&gt;🔗 &lt;a href="https://monkeytype-analysis.heterl0.live/" rel="noopener noreferrer"&gt;My Website for Analysis&lt;/a&gt;🔗 &lt;a href="https://heterl0.live/feed/feed.xml" rel="noopener noreferrer"&gt;Follow My Blog Feed&lt;/a&gt;&lt;/p&gt;

</description>
      <category>monkeytype</category>
      <category>extension</category>
      <category>nextjs</category>
      <category>touchtyping</category>
    </item>
    <item>
      <title>Build personal blog easy with 11ty.js</title>
      <dc:creator>Văn Hiếu Lê</dc:creator>
      <pubDate>Thu, 20 Feb 2025 12:00:00 +0000</pubDate>
      <link>https://dev.to/heterl0/build-personal-blog-easy-with-11tyjs-388o</link>
      <guid>https://dev.to/heterl0/build-personal-blog-easy-with-11tyjs-388o</guid>
      <description>&lt;h2&gt;
  
  
  Introduce
&lt;/h2&gt;

&lt;p&gt;At the end of 2024, after graduating from university, I wanted to build something meaningful using the skills and experience gained from my job. Initially, I planned to create a portfolio, but I wasn’t ready to design it. I also wanted something original rather than combining or copying existing designs. So, I decided to start with a personal blog instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Eleventy?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.11ty.dev/" rel="noopener noreferrer"&gt;Eleventy&lt;/a&gt; (or 11ty) is a simple and fast static site generator that allows you to build websites quickly. It supports multiple templating languages, including HTML, Markdown, and Nunjucks. This makes it easy to deliver content quickly to readers while also simplifying deployment and maintenance.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fheterl0.live%2Fblog%2Fbuild-personal-blog-with-eleventy%2F11ty-logo.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%2Fheterl0.live%2Fblog%2Fbuild-personal-blog-with-eleventy%2F11ty-logo.png" title="11ty Logo" alt="11ty Logo" width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Why I choose 11ty?
&lt;/h3&gt;

&lt;p&gt;At work, I often use React and Next.js for projects. Some projects utilize Next.js as a headless CMS with WordPress for content delivery. This combination of business logic from Next.js and the powerful content management features of WordPress is very effective.&lt;/p&gt;

&lt;p&gt;Initially, I wanted to create a blog using Next.js with Firebase and deploy it on Vercel. This seemed like a good approach for combining a portfolio and a blog.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Powerful Static Site Generation:&lt;/strong&gt; Next.js could generate static sites efficiently by fetching content from Firebase. This setup would also allow me to write and store blog posts directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rich Library Support:&lt;/strong&gt; Combining my portfolio and blog into one platform would enable advanced customizations using the Next.js ecosystem.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Disadvantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free Tier Limitations:&lt;/strong&gt; Vercel's free tier limits deployments to 12 serverless functions, and each Firebase data fetch counts as one. This could be a problem as the site grows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complexity:&lt;/strong&gt; The integration of Next.js with Firebase requires significant time and effort to set up and maintain, which could slow my progress.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I chose 11ty because it’s a simpler static site generator that aligns well with my goal of focusing on blog content.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Speed:&lt;/strong&gt; Eleventy is incredibly fast, allowing me to deploy a blog quickly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexible Templating:&lt;/strong&gt; It supports multiple templating languages, which makes customization easy.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Disadvantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Limited Scope:&lt;/strong&gt; Eleventy is primarily suited for blogs and simple static sites. It lacks advanced interactivity and business logic capabilities.
## Start with a &lt;a href="https://www.11ty.dev/docs/starter/" rel="noopener noreferrer"&gt;Starter Projects — Eleventy&lt;/a&gt;
When starting an 11ty project, there are many templates available to help you get started quickly. These templates provide pre-configured setups for various use cases, such as blogs, portfolios, and more.
&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%2Fe5l3rmw1xnferod69b1s.png" alt="Templates 11ty projects with high score lighthouse" width="800" height="321"&gt;
I chose the &lt;code&gt;Official Starter&lt;/code&gt; template to begin. &lt;a href="https://github.com/11ty/eleventy-base-blog" rel="noopener noreferrer"&gt;Link to the template&lt;/a&gt;.
After selecting the "Official Starter" template, I followed the instructions to set up my project. This involved installing dependencies, initializing Git, and running a few commands to get everything up and running.
## How to Write Posts?
I use &lt;a href="https://obsidian.md/" rel="noopener noreferrer"&gt;Obsidian&lt;/a&gt; to write posts and sync them to the &lt;code&gt;content/blog/&lt;/code&gt; directory in my project. Writing in &lt;code&gt;Markdown&lt;/code&gt; makes it easy to maintain posts. Here’s how I do it in a few steps:
### Step 1: Write the Post in Obsidian
I create a folder in Vault's Obsidian name &lt;code&gt;public&lt;/code&gt; will store my posts .
Change setting Obsidian a little bit in &lt;code&gt;File and links&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Turn off &lt;code&gt;Use Wikilinks&lt;/code&gt;, 11ty only render image or link follow the format of &lt;code&gt;.md&lt;/code&gt; not follow &lt;code&gt;Obsidian&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;Default location for new attachments&lt;/code&gt;: Choose &lt;code&gt;Same folder as current file&lt;/code&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%2Fg2qm2dvyznkphplhg7cp.png" alt="Adjust the setting" width="800" height="585"&gt;
| &lt;strong&gt;Note:&lt;/strong&gt; If a post doesn't contain images, you can create a single Markdown file. For posts with images, create a folder and name it the same as the Markdown file.
#### Metadata for Posts
You can add metadata for post with two ways:&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File Property:&lt;/strong&gt; Add each property individually in Obsidian.
&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%2Fqid5a043rjw5a45bnvo9.png" width="800" height="543"&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Source Mode:&lt;/strong&gt; Switch the file to source mode and add metadata like this:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;New&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Process&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Update"&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;blog&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;about&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;update&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;my&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;learn&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;typing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;touch&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;process"&lt;/span&gt;
&lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2025-01-05&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;touch-typing&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;| &lt;strong&gt;Tip:&lt;/strong&gt; The file name serves as the &lt;code&gt;slug&lt;/code&gt; for the post, so choose it carefully.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2: Sync the posts from Obsidian to 11ty source
&lt;/h3&gt;

&lt;p&gt;I use Windows, so I run the following &lt;code&gt;bash&lt;/code&gt; command in the terminal to sync my posts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;robocopy &lt;span class="s2"&gt;"My Vaults Obsidian Public"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\e&lt;/span&gt;&lt;span class="s2"&gt;leventy-sample&lt;/span&gt;&lt;span class="se"&gt;\c&lt;/span&gt;&lt;span class="s2"&gt;ontent&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="s2"&gt;log"&lt;/span&gt; /MIR

pause
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After syncing, check and test the build before proceeding to the next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment
&lt;/h2&gt;

&lt;p&gt;11ty is a simple static site generator. Once the site is built, the generated files are stored in the &lt;code&gt;_site/&lt;/code&gt; directory. You can deploy them using platforms like Vercel, Netlify, and others. Refer to the &lt;a href="https://www.11ty.dev/docs/deployment/" rel="noopener noreferrer"&gt;11ty Deployment Guide&lt;/a&gt; for more details.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploying with Netlify
&lt;/h3&gt;

&lt;p&gt;I use Netlify for my project. After signing in with GitHub or GitLab, I added the project to Netlify. The build process automatically starts, and the site is deployed with a subdomain provided by Netlify.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding a Custom Domain
&lt;/h3&gt;

&lt;p&gt;In your 11ty project settings on Netlify, go to &lt;strong&gt;Domain Management&lt;/strong&gt; and follow Netlify's instructions to add your custom domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This post shares my experience building a blog site with 11ty. If you notice any errors or have suggestions for improvement, feel free to contact me via &lt;a href="https://heterl0.live/about" rel="noopener noreferrer"&gt;Contact Page&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>11ty</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How to set up WordPress with Nginx and SSL</title>
      <dc:creator>Văn Hiếu Lê</dc:creator>
      <pubDate>Wed, 12 Feb 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/heterl0/how-to-set-up-wordpress-with-nginx-and-ssl-4dn3</link>
      <guid>https://dev.to/heterl0/how-to-set-up-wordpress-with-nginx-and-ssl-4dn3</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%2Fwq934exa1m2h737ygoz8.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%2Fwq934exa1m2h737ygoz8.png" alt="My Journey in Setting Up WordPress with Nginx and SSL" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;As a front-end developer, I’ve always focused on building great user interfaces, but recently, I’ve been diving deeper into server management and deployment. In this blog, I’ll share my personal experience setting up WordPress on an Nginx server with SSL, a journey that has helped me understand web hosting, security, and performance optimization. Check me project &lt;a href="https://github.com/heterl0/vovinam-fusion-wp" rel="noopener noreferrer"&gt;here&lt;/a&gt;!.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Chose This Setup
&lt;/h2&gt;

&lt;p&gt;I wanted a reliable and secure setup for hosting WordPress sites, and after researching, I decided to use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ubuntu 22.04 LTS&lt;/strong&gt; for stability and security&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nginx&lt;/strong&gt; for better performance compared to Apache&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSL (Let’s Encrypt)&lt;/strong&gt; for security and HTTPS support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This combination ensures that my WordPress sites are fast, secure, and scalable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step-by-Step Guide
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Setting Up the Server
&lt;/h3&gt;

&lt;p&gt;I started by updating my system and installing necessary packages:&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="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, I installed PHP, Nginx, and MySQL:&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="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nginx mysql-server php-fpm php-mysql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This provided the foundation for running WordPress.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Installing and Configuring WordPress
&lt;/h3&gt;

&lt;p&gt;I downloaded and extracted WordPress:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget https://wordpress.org/latest.tar.gz
&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xvzf&lt;/span&gt; latest.tar.gz
&lt;span class="nb"&gt;sudo mv &lt;/span&gt;wordpress /var/www/wordpress
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I set the correct file permissions:&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="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; www-data:www-data /var/www/wordpress/
&lt;span class="nb"&gt;sudo chmod&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; 755 /var/www/wordpress/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Setting Up the Database
&lt;/h3&gt;

&lt;p&gt;Using MySQL, I created a new database and user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;wordpress_db&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'wordpress_user'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'localhost'&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'securepassword'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;wordpress_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'wordpress_user'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'localhost'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;FLUSH&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Configuring Nginx
&lt;/h3&gt;

&lt;p&gt;I configured Nginx to serve WordPress efficiently. I edited the configuration file:&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="nb"&gt;sudo &lt;/span&gt;nano /etc/nginx/sites-available/wordpress
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Added the following configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;server {
    listen 80;
    server_name example.com;
    root /var/www/wordpress;
    index index.php index.html index.htm;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After saving, I enabled the configuration and restarted Nginx:&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="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/nginx/sites-available/wordpress /etc/nginx/sites-enabled/
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Enabling SSL with Let’s Encrypt
&lt;/h3&gt;

&lt;p&gt;To secure the site with HTTPS, I installed Certbot:&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="nb"&gt;sudo &lt;/span&gt;snap &lt;span class="nb"&gt;install &lt;/span&gt;core&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;snap refresh core
&lt;span class="nb"&gt;sudo &lt;/span&gt;snap &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--classic&lt;/span&gt; certbot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I obtained an SSL certificate and configured Nginx:&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="nb"&gt;sudo &lt;/span&gt;certbot &lt;span class="nt"&gt;--nginx&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; example.com &lt;span class="nt"&gt;-d&lt;/span&gt; www.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To ensure automatic renewal:&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="nb"&gt;sudo &lt;/span&gt;certbot renew &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File Permissions Matter&lt;/strong&gt; : WordPress won’t work correctly if permissions are wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database Security Is Important&lt;/strong&gt; : Using a secure password and restricting privileges is crucial.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nginx Configuration Can Be Tricky&lt;/strong&gt; : Testing with &lt;code&gt;nginx -t&lt;/code&gt; before restarting saved me a lot of debugging time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSL Is a Must&lt;/strong&gt; : Modern websites need HTTPS for security and SEO benefits.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Setting up WordPress with Nginx and SSL was a rewarding experience. Not only did it help me understand backend technologies, but it also made me appreciate the work that goes into web hosting and security. I hope this guide helps anyone looking to set up their own WordPress site with a strong, secure foundation.&lt;/p&gt;

&lt;p&gt;This is just the beginning—next, I plan to explore performance optimizations and caching strategies. Stay tuned! 🚀&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>nginx</category>
      <category>ssl</category>
      <category>deploy</category>
    </item>
  </channel>
</rss>
