<?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: Paul Knulst</title>
    <description>The latest articles on DEV Community by Paul Knulst (@paulknulst).</description>
    <link>https://dev.to/paulknulst</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%2F820180%2F088bfa04-cf35-447a-851c-e09ff4d45bb9.png</url>
      <title>DEV Community: Paul Knulst</title>
      <link>https://dev.to/paulknulst</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/paulknulst"/>
    <language>en</language>
    <item>
      <title>Self-Host Umami Analytics With Docker Compose</title>
      <dc:creator>Paul Knulst</dc:creator>
      <pubDate>Thu, 27 Mar 2025 07:16:26 +0000</pubDate>
      <link>https://dev.to/paulknulst/self-host-umami-analytics-with-docker-compose-2242</link>
      <guid>https://dev.to/paulknulst/self-host-umami-analytics-with-docker-compose-2242</guid>
      <description>&lt;p&gt;&lt;strong&gt;How To Track Website Analytics And Respect Data Privacy With Umami&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.paulsblog.dev/author/paulknulst/" rel="noopener noreferrer"&gt;Paul Knulst&lt;/a&gt;  in  &lt;a href="https://www.paulsblog.dev/tag/self-hosted/" rel="noopener noreferrer"&gt;Self-Hosted&lt;/a&gt; • Feb 9, 2024 • 8 min read&lt;/p&gt;




&lt;ol&gt;
&lt;li&gt; Introduction
&lt;/li&gt;
&lt;li&gt; Prerequisites

&lt;ol&gt;
&lt;li&gt; Folder Structure
&lt;/li&gt;
&lt;li&gt; The Environment File
&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt; Deploy Umami Using Compose For Single Instance Deployment

&lt;ol&gt;
&lt;li&gt; Run the Compose Services
&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt; Deploy Umami Using Compose For Docker Swarm Deployment

&lt;ol&gt;
&lt;li&gt; Run the Docker Swarm Stack
&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt; Post Installation Tasks
&lt;/li&gt;

&lt;li&gt; Closing Notes
&lt;/li&gt;

&lt;/ol&gt;

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

&lt;p&gt;Umami is an open-source, privacy-centric, and lightweight web analytics service built with JavaScript (NextJS) running in a NodeJS environment. It offers a fantastic alternative for those who want to be free from conventional analytics platforms that track your data and more importantly your visitors.&lt;/p&gt;

&lt;p&gt;Another thing that makes Umami really special is its user-friendly design, making it the ideal choice for your self-hosting alternative to Google Analytics. In this tutorial, you will learn how to unleash Umami's potential in a straightforward setup using Docker Compose. There is no need for complicated configurations or complex processes because Umami is designed about simplicity to ensure an easy self-hosted experience.&lt;/p&gt;

&lt;p&gt;Furthermore, Umami is open-source and grants you full visibility and control, allowing you to customize it according to your unique requirements.&lt;/p&gt;

&lt;p&gt;If you think about analytics you are often concerned about privacy, aren't you? Luckily, &lt;strong&gt;Umami has got your back, prioritizing the protection of user data while delivering the analytics insights you desire&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Now, get ready to dive into this tutorial and learn how to set up Umami with Docker Compose. If you follow this and deploy it in your own cluster, you will not only have a privacy-focused analytics solution in your toolkit but also the satisfaction of being the master of your data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Let's hope this tutorial empowers you to elevate your web analytics game with Umami – the go-to open-source option for privacy-conscious developers like yourself.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;This tutorial will deploy Umami either on a server running Docker (with Docker Compose) or within a server cluster utilizing Docker Swarm.&lt;/p&gt;

&lt;p&gt;💡&lt;strong&gt;Note:&lt;/strong&gt; &lt;strong&gt;Whenever you read “Docker Swarm” we are actually talking about “&lt;/strong&gt;&lt;em&gt;*&lt;strong&gt;&lt;em&gt;Docker Swarm mode&lt;/em&gt;&lt;/strong&gt;*&lt;/em&gt;&lt;strong&gt;”. (not the deprecated product Docker swarm)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To deploy this the tutorial assumes that we already:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  have any kind of website to monitor&lt;/li&gt;
&lt;li&gt;  have Docker and Docker Compose installed&lt;/li&gt;
&lt;li&gt;  optionally have a server cluster utilizing Docker Swarm&lt;/li&gt;
&lt;li&gt;  have a domain to publish the deployed Umami instance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Unfortunately&lt;/em&gt;, this tutorial will not show how to configure a domain with a TLS certificate because I normally use Traefik to automatically create SSL-secured domains based on configuration in the Compose file. &lt;strong&gt;Luckily&lt;/strong&gt;, I have created several tutorials about how to set up your own Traefik Proxy on your server, or your server cluster:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On your single instance server using Docker:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.paulsblog.dev/how-to-setup-traefik-with-automatic-letsencrypt-certificate-resolver/" rel="noopener noreferrer"&gt;How to setup Traefik v2 with automatic Let’s Encrypt certificate resolver&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On your server cluster using Docker Swarm:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.paulsblog.dev/services-you-want-to-have-in-a-swarm-environment/#traefik" rel="noopener noreferrer"&gt;4 Important Services Everyone Should Deploy In A Docker Swarm&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;The first step is to create a folder that will contain all files needed to deploy Umami either on a single server or on a server cluster. The structure should look like this containing two files &lt;code&gt;.env&lt;/code&gt; and &lt;code&gt;docker-compose.yml&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;umami/
|-- .env
|-- docker-compose.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Environment File
&lt;/h3&gt;

&lt;p&gt;Switch to the &lt;code&gt;umami&lt;/code&gt; folder and create a new &lt;code&gt;.env&lt;/code&gt; file containing the following snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgresql://umami_user:umami_pass@db:5432/umami_db
&lt;span class="nv"&gt;DATABASE_TYPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgresql
&lt;span class="nv"&gt;HASH_SALT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;generate_a_random_salt

&lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;umami_db
&lt;span class="nv"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;umami_user
&lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;umami_pass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this snippet, we define the &lt;code&gt;DATABASE_URL&lt;/code&gt; which Umami will use. As we can see it uses the same values as &lt;code&gt;POSTGRES_DB&lt;/code&gt;, &lt;code&gt;POSTGRES_USER&lt;/code&gt;, and &lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt;. Keep this in mind if you want to change it. Additionally, you should generate a random salt and put it into the &lt;code&gt;HASH_SALT&lt;/code&gt; variable. To generate something just hit your keyboard or use this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl rand &lt;span class="nt"&gt;-base64&lt;/span&gt; 64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deploy Umami Using Compose For Single Instance Deployment
&lt;/h2&gt;

&lt;p&gt;Now, we can create our Docker Compose file which will be used to deploy the service. As a starting point for our configuration, we can download the Compose file from &lt;a href="https://github.com/umami-software/umami?ref=paulsblog.dev" rel="noopener noreferrer"&gt;Umami's GitHub repository&lt;/a&gt; which will get some modifications:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  We will remove the environment variables for the database because we extract them already into an &lt;code&gt;.env&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;  We add Traefik configuration to use automatic TLS certificate generation for a URL&lt;/li&gt;
&lt;li&gt;  We add &lt;code&gt;TRACKER_SCRIPT_NAME&lt;/code&gt; to avoid getting blocked by Ad-Blocker&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The resulting Compose file to deploy a single instance Docker using Traefik Proxy looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.7'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;umami&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/umami-software/umami:postgresql-latest&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;TRACKER_SCRIPT_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;getinfo&lt;/span&gt;
      &lt;span class="na"&gt;API_COLLECT_ENDPOINT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;
      &lt;span class="na"&gt;APP_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;replace-me-with-a-random-string&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.enable=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.docker.network=traefik-public&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.constraint-label=traefik-public&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.umami-http.rule=Host(`umami.${PRIMARY_DOMAIN}`)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.umami-http.entrypoints=http&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.umami-http.middlewares=https-redirect&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.umami-https.rule=Host(`umami.${PRIMARY_DOMAIN}`)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.umami-https.entrypoints=https&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.umami-https.tls=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.umami-https.tls.certresolver=le&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.services.umami.loadbalancer.server.port=3000&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik-public&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:15-alpine&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;traefik-public&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some important information:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  In this Compose file, we added several &lt;code&gt;labels&lt;/code&gt; that are needed to deploy the service using a Traefik Proxy which is installed within our Docker environment like it was described in this tutorial:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.paulsblog.dev/how-to-setup-traefik-with-automatic-letsencrypt-certificate-resolver/" rel="noopener noreferrer"&gt;How to setup Traefik v2 with automatic Let’s Encrypt certificate resolver&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  By adding &lt;code&gt;TRACKER_SCRIPT_NAME&lt;/code&gt; we can change the filename of the tracking script. This allows us to avoid being blocked by many ad blockers. I just called it &lt;code&gt;getinfo&lt;/code&gt; to have some very general name. This setting is totally optional but I recommend doing it. &lt;a href="https://github.com/umami-software/umami/discussions/1026?ref=paulsblog.dev" rel="noopener noreferrer"&gt;See this GitHub discussion&lt;/a&gt; for more information&lt;/li&gt;
&lt;li&gt;  The database container is not accessible from the public network and only communicates with the umami web container through the &lt;code&gt;db&lt;/code&gt; network&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;umami&lt;/code&gt; web container is part of the external network &lt;code&gt;traefik-public&lt;/code&gt; (see previous post) and the &lt;code&gt;default&lt;/code&gt; network to communicate with the &lt;code&gt;db&lt;/code&gt; container.&lt;/li&gt;
&lt;li&gt;  All data is persisted within a named volume called &lt;code&gt;db&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Run the Compose Services
&lt;/h3&gt;

&lt;p&gt;After all files are created we have to define our &lt;code&gt;PRIMARY_DOMAIN&lt;/code&gt; environment variable by exporting it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PRIMARY_DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;paulsblog.dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we can start the container by executing:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;💡 &lt;strong&gt;Note:&lt;/strong&gt; If using a newer version of Docker and Docker Compose the command could be &lt;code&gt;docker compose up -d&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Switch to &lt;a href="https://umami.PRIMARY%5C_DOMAIN" rel="noopener noreferrer"&gt;https://umami.PRIMARY\_DOMAIN&lt;/a&gt; and log in with the default credentials: &lt;code&gt;admin:umami&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;To stop the container at any time we have to switch to the umami folder and execute the following:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deploy Umami Using Compose For Docker Swarm Deployment
&lt;/h2&gt;

&lt;p&gt;To set up Umami within a Docker Swarm environment we have to adjust the previously explained Docker Compose file by adding the deploy keyword. Additionally, we have to add placement constraints for the database service to guarantee that it is deployed on the correct server within the Docker Swarm cluster.&lt;/p&gt;

&lt;p&gt;The resulting Docker Compose file will be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.7'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;umami&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/umami-software/umami:postgresql-latest&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;TRACKER_SCRIPT_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;getinfo&lt;/span&gt;
      &lt;span class="na"&gt;API_COLLECT_ENDPOINT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;
      &lt;span class="na"&gt;APP_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;replace-me-with-a-random-string&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.enable=true&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.docker.network=traefik-public&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.constraint-label=traefik-public&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.umami-http.rule=Host(`umami.${PRIMARY_DOMAIN}`)&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.umami-http.entrypoints=http&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.umami-http.middlewares=https-redirect&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.umami-https.rule=Host(`umami.${PRIMARY_DOMAIN}`)&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.umami-https.entrypoints=https&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.umami-https.tls=true&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.umami-https.tls.certresolver=le&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.services.umami.loadbalancer.server.port=3000&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik-public&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:15-alpine&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;placement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;constraints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node.labels.umami.db == &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;traefik-public&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this file, we have moved all &lt;code&gt;labels&lt;/code&gt; below the &lt;code&gt;deploy&lt;/code&gt; keyword to enable Traefik configuration while utilizing a Docker Swarm environment. Furthermore, we have added the deploy keyword containing placement constraints in the &lt;code&gt;db&lt;/code&gt; service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;placement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;constraints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node.labels.umami.db == &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This constraint will ensure that the db service is always deployed on the worker node within the Docker Swarm which has the corresponding label. To set the label (if not already done) we have to execute the following command on the Docker Swarm Manager Node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker node update &lt;span class="nt"&gt;--label-add&lt;/span&gt; umami.db&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;ID_OF_NODE_TO_USE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;💡 &lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;ID_OF_NODE_TO_USE&lt;/code&gt; should be the node of the server that will be used. Use &lt;code&gt;docker node ls&lt;/code&gt; on the manager node to find the IDs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Run the Docker Swarm Stack
&lt;/h3&gt;

&lt;p&gt;To deploy the service in our Docker Swarm we have to export the &lt;code&gt;PRIMARY_DOMAIN&lt;/code&gt; by using &lt;code&gt;export PRIMARY_DOMAIN=paulsblog.dev&lt;/code&gt; in our CLI and then execute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker-compose config | docker stack deploy -c - NAME_OF_STACK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command will use the Compose &lt;code&gt;config&lt;/code&gt; command to load the &lt;code&gt;.env&lt;/code&gt; file settings into the Compose file before using &lt;code&gt;docker stack deploy&lt;/code&gt; to deploy the stack in our Docker Swarm environment. Change &lt;code&gt;NAME_OF_STACK&lt;/code&gt; to an appropriate name lime &lt;code&gt;umami&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;💡&lt;strong&gt;Note:&lt;/strong&gt; If you for any reason change the name of the Compose file from &lt;code&gt;docker-compose.yml&lt;/code&gt; to something like &lt;code&gt;docker-compose.umami.yml&lt;/code&gt; (or anything) the deploy command will fail. *&lt;strong&gt;&lt;em&gt;It only works correctly if you keep the name!&lt;/em&gt;&lt;/strong&gt;*&lt;/p&gt;

&lt;p&gt;To stop the stack use &lt;code&gt;docker stack rm NAME_OF_STACK&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Post Installation Tasks
&lt;/h2&gt;

&lt;p&gt;Now, as our Umami instance is running we should log in and instantly change our Umami credentials from &lt;code&gt;admin:umami&lt;/code&gt; to something more secure.&lt;/p&gt;

&lt;p&gt;Then we use the Add Site button in the top right corner to add our website to track&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%2Fi8ytmnajemyxk6wlni9k.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi8ytmnajemyxk6wlni9k.jpg" alt="Self-Host Umami Analytics With Docker Compose - Add a website" width="325" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Adding a new website to Umami analytics&lt;/p&gt;

&lt;p&gt;Now, we can switch to &lt;strong&gt;&lt;em&gt;Tracking code&lt;/em&gt;&lt;/strong&gt; in the tab layout.&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%2Ff2zdvz2k1az87j32gt9l.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff2zdvz2k1az87j32gt9l.jpg" alt="Self-Host Umami Analytics With Docker Compose - Tracking code menu item" width="376" height="67"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The tab layout menu in the top of Umami analytics website&lt;/p&gt;

&lt;p&gt;The tracking code looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&amp;lt;script async &lt;span class="nv"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://umami.paulsblog.dev/getbooks"&lt;/span&gt; data-website-id&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sd2dasa-5231-7h7f-9jj8-15ab565f8af9"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I would recommend adding the &lt;code&gt;defer&lt;/code&gt; keyword to it and then insert it in the header section of your website. Afterward, tracking of our website will start and we can instantly see our visitors in the real-time view in Umami&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%2Fviaw6ub2gv3nlmw1zwy4.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fviaw6ub2gv3nlmw1zwy4.jpg" alt="Self-Host Umami Analytics With Docker Compose - Realtime Dashboard" width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Umami analytics realtime dashboard for paulsblog.dev&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Notes
&lt;/h2&gt;

&lt;p&gt;To sum up, Umami really impresses me with its straightforward approach to website analytics, particularly when compared with other self-hosted alternatives like Countly, which I had previously used and found to be overly bloated. Furthermore, one of Umami's standout features is its straightforward setup process using Docker, making it accessible even to those Developers less familiar with advanced technical concepts.&lt;/p&gt;

&lt;p&gt;Another important aspect of Umami is its trustworthy option, prioritizing user data protection without sacrificing functionality. As a software developer, it is paramount to understand that selecting analytics tools that not only provide valuable insights but also prioritize the security and privacy of user information is mandatory. By choosing Umami, we can ensure that our analytics remain both effective and ethically correct in today's digital landscape.&lt;/p&gt;

&lt;p&gt;Do you have any questions regarding this tutorial? I would love to hear your thoughts and answer your questions. Please share everything in the comments.&lt;/p&gt;

&lt;p&gt;Feel free to connect with me on &lt;a href="https://medium.knulst.de/?ref=paulsblog.dev" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/paulknulst/?ref=paulsblog.dev" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://twitter.com/paulknulst?ref=paulsblog.dev" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;, and &lt;a href="https://github.com/paulknulst?ref=paulsblog.dev" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thank you for reading, and &lt;strong&gt;happy analyzing your data!&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;🙌 Support this content&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you like this content, please consider &lt;a href="https://www.paulsblog.dev/support/" rel="noopener noreferrer"&gt;supporting me&lt;/a&gt;. You can share it on social media or &lt;a href="https://buymeacoffee.com/paulknulst" rel="noopener noreferrer"&gt;buy me a coffee&lt;/a&gt;! Any support helps!&lt;/p&gt;

&lt;p&gt;Furthermore, you can &lt;a href="https://www.paulsblog.dev/#/portal/signup/" rel="noopener noreferrer"&gt;sign up for my newsletter&lt;/a&gt; to show your contribution to my content. See the &lt;a href="https://www.paulsblog.dev/contribute/" rel="noopener noreferrer"&gt;contribute page&lt;/a&gt; for all (free or paid) ways to say thank you!&lt;/p&gt;

&lt;p&gt;Thanks! 🥰&lt;/p&gt;




&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@dawson2406?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Stephen Dawson&lt;/a&gt; / &lt;a href="https://unsplash.com/photos/turned-on-monitoring-screen-qwtCeJ5cLYs?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>My newest article :)</title>
      <dc:creator>Paul Knulst</dc:creator>
      <pubDate>Fri, 21 Mar 2025 09:13:02 +0000</pubDate>
      <link>https://dev.to/paulknulst/my-newest-article--1ekd</link>
      <guid>https://dev.to/paulknulst/my-newest-article--1ekd</guid>
      <description>&lt;div class="ltag__link"&gt;
  &lt;a href="/paulknulst" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&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%2Fuser%2Fprofile_image%2F820180%2F088bfa04-cf35-447a-851c-e09ff4d45bb9.png" alt="paulknulst"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/paulknulst/how-to-manage-docker-logfiles-with-logrotate-16ai" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;How To Manage Docker Logfiles with Logrotate&lt;/h2&gt;
      &lt;h3&gt;Paul Knulst ・ Mar 21&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#programming&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#docker&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#webdev&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#softwareengineering&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


</description>
      <category>programming</category>
      <category>docker</category>
      <category>webdev</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>How To Manage Docker Logfiles with Logrotate</title>
      <dc:creator>Paul Knulst</dc:creator>
      <pubDate>Fri, 21 Mar 2025 07:32:59 +0000</pubDate>
      <link>https://dev.to/paulknulst/how-to-manage-docker-logfiles-with-logrotate-16ai</link>
      <guid>https://dev.to/paulknulst/how-to-manage-docker-logfiles-with-logrotate-16ai</guid>
      <description>&lt;p&gt;&lt;strong&gt;It is essential to configure log rotation for Docker containers. Log rotation is not performed by default, and if it’s not configured, logs on the Docker host can build up and eat up disk space. This guide will teach us how to set up Docker log rotation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.paulsblog.dev/author/paulknulst/" rel="noopener noreferrer"&gt;Paul Knulst&lt;/a&gt;  in  &lt;a href="https://www.paulsblog.dev/tag/docker/" rel="noopener noreferrer"&gt;Docker&lt;/a&gt; • Dec 2, 2022 • 5 min read&lt;/p&gt;




&lt;ol&gt;
&lt;li&gt; Introduction
&lt;/li&gt;
&lt;li&gt; The Problem
&lt;/li&gt;
&lt;li&gt; A Brief On Docker Logs
&lt;/li&gt;
&lt;li&gt; Where are Docker logs stored?
&lt;/li&gt;
&lt;li&gt; Docker Log Rotation To The Rescue
&lt;/li&gt;
&lt;li&gt; Configuring Log Rotation For Specific Containers

&lt;ol&gt;
&lt;li&gt; Log Rotation With Docker Run
&lt;/li&gt;
&lt;li&gt; Log rotation in Docker Compose
&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt; Closing Notes
&lt;/li&gt;

&lt;/ol&gt;

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

&lt;p&gt;Logs are important and are an essential piece of telemetry data. Logs contain all kinds of events that take place in your application and can be used to debug or troubleshoot issues. Often it is possible to identify the root cause of a problem by having good logs.&lt;/p&gt;

&lt;p&gt;Using Docker and Docker Compose to containerize and scale applications is very easy nowadays. Unfortunately, operation complexity has increased, and frequently changing container-based environments are challenging to monitor.&lt;/p&gt;

&lt;p&gt;Luckily Docker can display container logs that are generated through &lt;code&gt;stdout&lt;/code&gt; and &lt;code&gt;stderr&lt;/code&gt; with different logging drivers (&lt;code&gt;local&lt;/code&gt;, &lt;code&gt;json-file&lt;/code&gt;, &lt;code&gt;syslog&lt;/code&gt;, &lt;code&gt;awslog&lt;/code&gt;, etc). As default, Docker uses the &lt;code&gt;json-file&lt;/code&gt; logging driver. This logging driver caches container logs and saves them to the disk.&lt;/p&gt;

&lt;p&gt;But before deep-diving into Docker logs, adjusting the default Docker log settings, and enabling Docker log rotation I will briefly describe what happened to me and why it could lead to big problems for you if you do not enable Logrotate for your container.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;It is 10:30 pm and I am working on my private Docker Swarm cluster. I just finished a cool new service and start to build and deploy it. I already closed every terminal, exited the IDE, and want to turn off my computer to finally relax. But, THE BUILDS ARE FAILING. Oh no...&lt;/p&gt;

&lt;p&gt;I cannot push or pull to my self-hosted private docker registry. But the containers are still running without any problem.&lt;/p&gt;

&lt;p&gt;I asked myself why it is not possible to read/write to my registry until I realized that the disk is full. I opened a terminal and connect to my machine where I checked how much space is left. &lt;strong&gt;Unfortunately, nothing...&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As I did not have any clue I searched on my disk which folder is using so much space and after some time it turns out the Docker folder is big. Nearly 400GB big. Why is it so big?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The answer:&lt;/strong&gt; logs. a LOT of logs. Really, a LOT!&lt;/p&gt;

&lt;p&gt;Unfortunately, I was not able to simply delete the Docker folders because it could harm my services so I was forced to research Docker logs and how I am able to fix my problem.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;In the end, I fixed it on the same day (But, it was really close to midnight)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A Brief On Docker Logs
&lt;/h2&gt;

&lt;p&gt;In Docker are two types of log files.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Docker daemon logs:&lt;/strong&gt; Generated by the Docker daemon and saved on the host. This log file provided information about the state of the Docker platform.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Docker container logs:&lt;/strong&gt; Docker container logs contains every log entry related to any application running in a container.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Unfortunately, Docker does not have any size restriction on logging files. This means they will increase over time and consume more and more storage if left unhandled. Every service that stays online and produces a large number of log messages, will consume more disk space over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where are Docker logs stored?
&lt;/h2&gt;

&lt;p&gt;Normally, logs are kept on the Docker host because the containers are stateless which means they won't save data if they are rebuilt/restarted. As default, Docker uses the &lt;code&gt;json-file&lt;/code&gt; logging driver which records all &lt;code&gt;stdout&lt;/code&gt;/&lt;code&gt;stderr&lt;/code&gt; output in JSON format and create a file for each container at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/var/lib/docker/containers/[container-id]/[container-id]-JSON.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;With this information in mind, I was able to delete the biggest log files and free up 200GB of disk space. Funnily, it was only 1 container that had this amount of logs but it was running for nearly one year and I totally forgot about it.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker Log Rotation To The Rescue
&lt;/h2&gt;

&lt;p&gt;To be able to rotate logs produced by the Docker container I first had to configure the Docker daemon to use a particular log driver. This could be done by editing the &lt;code&gt;daemon.json&lt;/code&gt; on the Docker host.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; Switch to the Docker daemon configuration file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  On Linux: &lt;code&gt;/etc/docker/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  On Windows: &lt;code&gt;C:\ProgramData\docker\config\daemon.json&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; If no logging driver is set, use the following command to set it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"log-driver"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"json-file"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;You can also use another log driver but I prefer&lt;/em&gt; &lt;code&gt;json-file&lt;/code&gt;&lt;em&gt;. Docker recommends&lt;/em&gt; &lt;code&gt;local&lt;/code&gt; &lt;em&gt;because it performs log rotation by default.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; As I choose &lt;code&gt;json-file&lt;/code&gt; I have to add an additional configuration for log rotation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;  
    &lt;/span&gt;&lt;span class="nl"&gt;"log-driver"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"json-file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  
    &lt;/span&gt;&lt;span class="nl"&gt;"log-opts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;    
        &lt;/span&gt;&lt;span class="nl"&gt;"max-size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"15m"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;    
        &lt;/span&gt;&lt;span class="nl"&gt;"max-file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;    
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 4: Save the file and restart the docker daemon:&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;systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Although the &lt;code&gt;daemon.json&lt;/code&gt; was updated and the Docker daemon was restarted it will not work for any running container because the default logging driver will only impact containers that are created after modifying it. Existing containers will still work with the initial configuration which was &lt;code&gt;json-file&lt;/code&gt; without any log rotation!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In order to enable log rotating for existing containers, they have to be recreated. Afterward, they will use log rotation!&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring Log Rotation For Specific Containers
&lt;/h2&gt;

&lt;p&gt;Instead of adjusting the Docker daemon's default logging driver, it is possible to configure a single Docker container to use a different logging driver and enable log rotation. This can be done while creating the container with Docker run and within a Docker Compose file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Log Rotation With Docker Run
&lt;/h3&gt;

&lt;p&gt;To run a new container with another logging driver you need to include the &lt;code&gt;--log-driver&lt;/code&gt; flag while using &lt;code&gt;docker run&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--log-driver&lt;/span&gt; json-file nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This snippet defines &lt;code&gt;json-file&lt;/code&gt; as the logging driver and does not configure log rotation.&lt;/p&gt;

&lt;p&gt;To find out which logging driver is currently used by any container you can use the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker inspect &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'{{.HostConfig.LogConfig.Type}}'&lt;/span&gt; &amp;lt;CONTAINER&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To set up a &lt;code&gt;json-file&lt;/code&gt; logging driver with log rotation enabled use this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run  &lt;span class="nt"&gt;--log-opt&lt;/span&gt; max-size&lt;span class="o"&gt;=&lt;/span&gt;15m &lt;span class="nt"&gt;--log-opt&lt;/span&gt; max-file&lt;span class="o"&gt;=&lt;/span&gt;5 nginx:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Simplifying the above command:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--log-opt max-size=15m&lt;/code&gt;: Set the maximum file size for your Docker log file.&lt;br&gt;&lt;br&gt;
&lt;code&gt;--log-opt max-file=5&lt;/code&gt;: Sets the maximum amount of log files to use. If the limit is reached, Docker will delete the oldest one and starts from the beginning.&lt;br&gt;&lt;br&gt;
&lt;code&gt;nginx:latest&lt;/code&gt;: The container image name&lt;/p&gt;
&lt;h3&gt;
  
  
  Log rotation in Docker Compose
&lt;/h3&gt;

&lt;p&gt;If you are using Docker Compose to run your Docker containers you can open up any Compose file and add a new section containing the logging driver and log rotate settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;webserver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:latest&lt;/span&gt;
    &lt;span class="na"&gt;logging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json-file"&lt;/span&gt;
      &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;max-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
        &lt;span class="na"&gt;max-size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Simplifying the Compose section:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;image: nginx:latest&lt;/code&gt;: The container image name&lt;br&gt;&lt;br&gt;
&lt;code&gt;driver&lt;/code&gt;: Sets the logging driver to use&lt;br&gt;&lt;br&gt;
&lt;code&gt;max-size: 15m&lt;/code&gt;: Set the maximum file size for your Docker log file.&lt;br&gt;&lt;br&gt;
&lt;code&gt;max-file: 5&lt;/code&gt;: Sets the maximum amount of log files to use. If the limit is reached, Docker will delete the oldest one and starts from the beginning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Notes
&lt;/h2&gt;

&lt;p&gt;In this guide, I showed how a small incident in my Docker environment forced me to learn about Docker log rotation.&lt;/p&gt;

&lt;p&gt;Luckily, it was very easy to set up log rotation for my Docker environment and have every new container use this approach. For existing containers, I manually had to adjust the Compose file and redeploy them as I use a Docker Swarm environment.&lt;/p&gt;

&lt;p&gt;I hope this article gave you a quick and neat overview of how to enable log rotation in your own Docker environment.&lt;/p&gt;

&lt;p&gt;Keep in mind that Docker container environments are highly dynamic with multiple layers of abstraction and it's hard to debug such environments. Because of this, many applications create an enormous amount of logs which play a critical role in providing information to identify issues. You will always need these logs and you should not deactivate them!&lt;/p&gt;

&lt;p&gt;How about you? Do you use log rotation for your Docker containers? Do you have any other approach? Also, do you have any questions regarding Docker logs? I would love to hear your thoughts. Please share everything in the comments.&lt;/p&gt;

&lt;p&gt;This article was originally published on my blog at &lt;a href="https://www.paulsblog.dev/how-to-manage-docker-logfiles-with-logrotate/" rel="noopener noreferrer"&gt;https://www.paulsblog.dev/how-to-manage-docker-logfiles-with-logrotate/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to connect with me on &lt;a href="https://www.paulsblog.dev" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;, &lt;a href="https://medium.knulst.de/" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/paulknulst/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://twitter.com/paulknulst" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;, and &lt;a href="https://github.com/paulknulst" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;🙌 Support this content&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you like this content, please consider &lt;a href="https://www.paulsblog.dev/support/" rel="noopener noreferrer"&gt;supporting me&lt;/a&gt;. You can share it on social media or &lt;a href="https://buymeacoffee.com/paulknulst" rel="noopener noreferrer"&gt;buy me a coffee&lt;/a&gt;! Any support helps!&lt;/p&gt;

&lt;p&gt;Furthermore, you can &lt;a href="https://www.paulsblog.dev/#/portal/signup/" rel="noopener noreferrer"&gt;sign up for my newsletter&lt;/a&gt; to show your contribution to my content. See the &lt;a href="https://www.paulsblog.dev/contribute/" rel="noopener noreferrer"&gt;contribute page&lt;/a&gt; for all (free or paid) ways to say thank you!&lt;/p&gt;

&lt;p&gt;Thanks! 🥰&lt;/p&gt;




&lt;p&gt;Image by &lt;a href="https://www.freepik.com/free-photo/still-life-business-roles-with-various-mechanism-pieces_24749608.htm#query=rotate%20message%20gear&amp;amp;position=49&amp;amp;from_view=search&amp;amp;track=ais" rel="noopener noreferrer"&gt;Freepik&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>docker</category>
      <category>webdev</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>How To Unhide Titlebars on Maximised Windows in KDE Plasma 6</title>
      <dc:creator>Paul Knulst</dc:creator>
      <pubDate>Tue, 19 Mar 2024 11:40:49 +0000</pubDate>
      <link>https://dev.to/paulknulst/how-to-unhide-titlebars-on-maximised-windows-in-kde-plasma-6-58id</link>
      <guid>https://dev.to/paulknulst/how-to-unhide-titlebars-on-maximised-windows-in-kde-plasma-6-58id</guid>
      <description>&lt;p&gt;Maybe Unimportant For You But Mandatory For Me&lt;/p&gt;

&lt;p&gt;Since I updated my Garuda Linux some days ago it switched from Plasma 5 to Plasma 6 which removed a Widget that I was using in my top panel: "Application Menu"&lt;/p&gt;

&lt;p&gt;This led to a major problem (for me) if I maximized my applications because instead of an application menu the Global Menu Widget was used. This can be seen in the following screenshot while maximizing the Brave browser:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--57HJbBwK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.paulsblog.dev/content/images/2024/03/linux-plasma-6-titlebars-for-brave-browser.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--57HJbBwK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.paulsblog.dev/content/images/2024/03/linux-plasma-6-titlebars-for-brave-browser.png" alt="KDE Plasma 6 Global Menu If Window is Maximized&amp;lt;br&amp;gt;
" width="680" height="48"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The problem was that the Global Menu Widget missed some adjustments that I needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extend the Menu to use the whole panel&lt;/li&gt;
&lt;li&gt;Enable Double Click to reset Full Screen&lt;/li&gt;
&lt;li&gt;Enable Click And Drag to move the Window to another screen maybe&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Normally, this wouldn't be a problem because every application has its own title bar. Unfortunately, the default behavior for my installation of Garuda Linux was configured that if I maximized any Window it would hide the title bar which led to the problem that for some applications I couldn't minimize, move or close them through the top bar. See the marked spots on the following screenshot:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--amqFBy0P--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.paulsblog.dev/content/images/size/w1000/2024/03/linux-plasma-6-titlebars-for-brave-browser-unclickable-area.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--amqFBy0P--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.paulsblog.dev/content/images/size/w1000/2024/03/linux-plasma-6-titlebars-for-brave-browser-unclickable-area.png" alt="KDE Plasma 6 Global Menu If Window is Maximized With Unclickable Area" width="800" height="18"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;em&gt;If using a Panel with the Global Menu Widget and Window Buttons Widget (see screenshot) I would have the functionality to minimize, and close if using the buttons.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Fortunately, I found out that there exists a hidden setting in KWin that can be set to enable and also disable title bars and window borders if the application window is maximized.&lt;/p&gt;

&lt;p&gt;To do this, I had to open the following file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.config/kwinrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this file, I searched for the section &lt;code&gt;[Windows]&lt;/code&gt; and changed the setting of &lt;code&gt;BorderlessMaximizedWindows&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt; so that it looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Windows]
BorderlessMaximizedWindows=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After logging out and logging in back again the title bars were available all the time and I could maximize on double click and move the window without any problems.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--MBYouEzD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.paulsblog.dev/content/images/2024/03/how-to-unhide-titlebars-on-maximised-windows-in-kde-plasma-6-solution-found.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--MBYouEzD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.paulsblog.dev/content/images/2024/03/how-to-unhide-titlebars-on-maximised-windows-in-kde-plasma-6-solution-found.jpg" alt="Image from https://imgflip.com/i/8jqu8y" width="613" height="407"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;As this was triggering me&lt;/strong&gt; the last few days and I searched some days to fix this problem I decided to create a small post for everyone who will ever face this problem.&lt;/p&gt;

&lt;p&gt;Also if you are one of these guys that do not want to have title bars at all, you can use this setting to remove them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So I guess, it is important for everyone :)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This article was originally published on my blog at &lt;a href="https://www.paulsblog.dev/how-to-unhide-titlebars-on-maximised-windows-in-kde-plasma-6/"&gt;https://www.paulsblog.dev/how-to-unhide-titlebars-on-maximised-windows-in-kde-plasma-6/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to connect with me on &lt;a href="https://www.paulsblog.dev"&gt;my blog&lt;/a&gt;, &lt;a href="https://medium.knulst.de/"&gt;Medium&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/paulknulst/"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://twitter.com/paulknulst"&gt;Twitter&lt;/a&gt;, and &lt;a href="https://github.com/paulknulst"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;🙌 Support this content&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you like this content, please consider &lt;a href="https://www.paulsblog.dev/support/"&gt;supporting me&lt;/a&gt;. You can share it on social media or &lt;a href="https://buymeacoffee.com/paulknulst"&gt;buy me a coffee&lt;/a&gt;! Any support helps!&lt;/p&gt;

&lt;p&gt;Furthermore, you can &lt;a href="https://www.paulsblog.dev/#/portal/signup/"&gt;sign up for my newsletter&lt;/a&gt; to show your contribution to my content. See the &lt;a href="https://www.paulsblog.dev/contribute/"&gt;contribute page&lt;/a&gt; for all (free or paid) ways to say thank you!&lt;/p&gt;

&lt;p&gt;Thanks! 🥰&lt;/p&gt;




&lt;p&gt;Photo by &lt;a href="https://www.freepik.com/free-photo/young-businessman-using-computer-office_13517371.htm#fromView=search&amp;amp;page=1&amp;amp;position=4&amp;amp;uuid=ce4c0261-b0b7-4d3a-9ec4-257be401335c"&gt;master1305&lt;/a&gt; / &lt;a href="https://www.freepik.com"&gt;Freepik&lt;/a&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>archlinux</category>
      <category>kde</category>
      <category>watercooler</category>
    </item>
    <item>
      <title>How To Set Up A Mailserver Within A Docker Swarm</title>
      <dc:creator>Paul Knulst</dc:creator>
      <pubDate>Fri, 19 Jan 2024 07:48:26 +0000</pubDate>
      <link>https://dev.to/paulknulst/how-to-set-up-a-mailserver-within-a-docker-swarm-2n76</link>
      <guid>https://dev.to/paulknulst/how-to-set-up-a-mailserver-within-a-docker-swarm-2n76</guid>
      <description>&lt;p&gt;&lt;strong&gt;Ever wanted to have your own mail server? Learn how to set up your own personal mail server with this step-by-step guide&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I run my own mail server to have generalized email addresses for different services.&lt;/p&gt;

&lt;p&gt;While I searched for a nice solution to put my mail server into a dockerized mail server I found the famous &lt;a href="https://github.com/docker-mailserver?ref=paulsblog.dev" rel="noopener noreferrer"&gt;docker-mailserver&lt;/a&gt;. Unfortunately, I could not use this mail server because I had many errors while configuring it.&lt;br&gt;&lt;br&gt;
Because of this, I searched for an alternative and found &lt;a href="https://github.com/hardware/mailserver?ref=paulsblog.dev" rel="noopener noreferrer"&gt;hardware-mailserver&lt;/a&gt; which is something like an optimized usage of a docker-mailserver with multiple predefined functionalities.&lt;/p&gt;

&lt;p&gt;If I had a normal docker environment without any other services I could use &lt;a href="https://github.com/hardware/mailserver/blob/master/docker-compose.sample.yml?ref=paulsblog.dev" rel="noopener noreferrer"&gt;this docker-compose.yml&lt;/a&gt; and start it by executing:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Because &lt;a href="https://www.paulsblog.dev/docker-swarm-in-a-nutshell/" rel="noopener noreferrer"&gt;I run a Docker Swarm&lt;/a&gt; environment with &lt;a href="https://www.paulsblog.dev/services-you-want-to-have-in-a-swarm-environment/#traefik" rel="noopener noreferrer"&gt;a Traefik load balancer&lt;/a&gt; that creates SSL certificates for my domains I have to make some adjustments within the Compose file to set up and configure the mail server. Every adjustment will be explained later on service by service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mailserver
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;mailserver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hardware/mailserver:1.1-stable&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MAILSERVER_RESTART_MODE}&lt;/span&gt;
    &lt;span class="na"&gt;domainname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MAILSERVER_DOMAIN}&lt;/span&gt;
    &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MAILSERVER_HOSTNAME}&lt;/span&gt; 
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;placement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;constraints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node.role == manager&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.enable=true&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.docker.network=traefik-public&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.constraint-label=traefik-public&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.frontend.rule=Host:mail.${MAILSERVER_DOMAIN}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.mailserver-spam-http.rule=Host(`${MAILSERVER_FQDN?Variable not set}`) || Host(`pop3.${MAILSERVER_DOMAIN?Variable not set}`) || Host(`smtp.${MAILSERVER_DOMAIN?Variable not set}`) || Host(`imap.${MAILSERVER_DOMAIN?Variable not set}`) || Host(`${MAILSERVER_DOMAIN_RSPAMD?Variable not set}`)&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.mailserver-spam-http.entrypoints=http&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.mailserver-spam-http.middlewares=https-redirect&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.mailserver-spam-https.rule=Host(`${MAILSERVER_FQDN?Variable not set}`) ||  Host(`pop3.${MAILSERVER_DOMAIN?Variable not set}`) || Host(`smtp.${MAILSERVER_DOMAIN?Variable not set}`) || Host(`imap.${MAILSERVER_DOMAIN?Variable not set}`) || Host(`${MAILSERVER_DOMAIN_RSPAMD?Variable not set}`)&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.mailserver-spam-https.entrypoints=https&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.mailserver-spam-https.tls=true&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.mailserver-spam-https.tls.certresolver=le&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.services.mailserver-spam.loadbalancer.server.port=11334&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;25:25"&lt;/span&gt;         &lt;span class="c1"&gt;# SMTP                - Required&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;110:110"&lt;/span&gt;     &lt;span class="c1"&gt;# POP3           STARTTLS - Optional - For webmails/desktop clients&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;143:143"&lt;/span&gt;     &lt;span class="c1"&gt;# IMAP           STARTTLS - Optional - For webmails/desktop clients&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;465:465"&lt;/span&gt;     &lt;span class="c1"&gt;# SMTPS         SSL/TLS  - Optional - Enabled for compatibility reason, otherwise disabled&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;587:587"&lt;/span&gt;     &lt;span class="c1"&gt;# Submission  STARTTLS - Optional - For webmails/desktop clients&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;993:993"&lt;/span&gt;     &lt;span class="c1"&gt;# IMAPS          SSL/TLS  - Optional - For webmails/desktop clients&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;995:995"&lt;/span&gt;     &lt;span class="c1"&gt;# POP3S          SSL/TLS  - Optional - For webmails/desktop clients&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4190:4190"&lt;/span&gt; &lt;span class="c1"&gt;# SIEVE            STARTTLS - Optional - Recommended for mail filtering&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;FQDN=${MAILSERVER_FQDN}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DOMAIN=${MAILSERVER_DOMAIN}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DBPASS=${MAILSERVER_DATABASE_USER_PASSWORD}&lt;/span&gt;       &lt;span class="c1"&gt;# MariaDB database password (required)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;RSPAMD_PASSWORD=${MAILSERVER_RSPAMD_PASSWORD}&lt;/span&gt;     &lt;span class="c1"&gt;# Rspamd WebUI password (required)&lt;/span&gt;
    &lt;span class="c1"&gt;#- ADD_DOMAINS=aa.tld, www.bb.tld...      # Add additional domains separated by commas (needed for dkim keys etc.)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ENABLE_POP3=true&lt;/span&gt;                       &lt;span class="c1"&gt;# Enable POP3 protocol&lt;/span&gt;
    &lt;span class="c1"&gt;#&lt;/span&gt;
    &lt;span class="c1"&gt;# Full list : https://github.com/hardware/mailserver#environment-variables&lt;/span&gt;
    &lt;span class="c1"&gt;#&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mail:/var/mail&lt;/span&gt;
      &lt;span class="c1"&gt;#- ./cert:/etc/letsencrypt/live/${MAILSERVER_FQDN}&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mariadb&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik-public&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the main service used by the mail server suite. The most important thing I had to add was the environment variables. Because I run a Docker Swarm setup hostname does not work correctly and I have to develop something else. Within a pull request on the GitHub page, I found a possible solution.&lt;/p&gt;

&lt;p&gt;I have to add &lt;code&gt;FQDN&lt;/code&gt; and &lt;code&gt;DOMAIN&lt;/code&gt; as environment variables. Another section I have to change is the labels section. I added the &lt;code&gt;deploy&lt;/code&gt; section and created two important properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;placement-constraint&lt;/code&gt; and&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;labels&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;placement-constraints&lt;/code&gt; are used to always deploy the mail server on my manager node and &lt;code&gt;labels&lt;/code&gt; is filled with all information that is needed for my Traefik instance.&lt;/p&gt;

&lt;p&gt;Finally, I have to change the volume entries. I want to use docker volumes instead of a local shared folder (which is achieved by adding a &lt;code&gt;./&lt;/code&gt; in front of the volume name)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also, there is one very important entry within the volumes section:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="c1"&gt;#- ./cert:/etc/letsencrypt/live/${MAILSERVER_FQDN}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I will explain what it does and why its comment out later in detail&lt;/p&gt;

&lt;h2&gt;
  
  
  Postfix-admin
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;postfixadmin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hardware/postfixadmin&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MAILSERVER_RESTART_MODE}&lt;/span&gt;
    &lt;span class="na"&gt;domainname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MAILSERVER_DOMAIN}&lt;/span&gt;
    &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MAILSERVER_HOSTNAME}&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;placement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;constraints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node.role == manager&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.enable=true&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.docker.network=traefik-public&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.constraint-label=traefik-public&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.mailserver-postfixadmin-http.rule=Host(`${MAILSERVER_DOMAIN_POSTFIXADMIN?Variable not set}`)&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.mailserver-postfixadmin-http.entrypoints=http&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.mailserver-postfixadmin-http.middlewares=https-redirect&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.mailserver-postfixadmin-https.rule=Host(`${MAILSERVER_DOMAIN_POSTFIXADMIN?Variable not set}`)&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.mailserver-postfixadmin-https.entrypoints=https&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.mailserver-postfixadmin-https.tls=true&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.mailserver-postfixadmin-https.tls.certresolver=le&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.services.mailserver-postfixadmin.loadbalancer.server.port=8888&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DBPASS=${MAILSERVER_DATABASE_USER_PASSWORD}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;FQDN=${MAILSERVER_FQDN}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DOMAIN=${MAILSERVER_DOMAIN}&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mailserver&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mariadb&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik-public&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Like I did before I have to add environment variables &lt;code&gt;FQDN&lt;/code&gt; and &lt;code&gt;DOMAIN&lt;/code&gt;, adjust the &lt;code&gt;placement-constraint&lt;/code&gt;, &lt;code&gt;labels&lt;/code&gt; and updated the volumes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rainloop
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;rainloop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hardware/rainloop&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MAILSERVER_RESTART_MODE}&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;placement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;constraints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node.role == manager&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.enable=true&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.docker.network=traefik-public&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.constraint-label=traefik-public&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.mailserver-rainloop-http.rule=Host(`${MAILSERVER_DOMAIN_RAINLOOP?Variable not set}`)&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.mailserver-rainloop-http.entrypoints=http&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.mailserver-rainloop-http.middlewares=https-redirect&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.mailserver-rainloop-https.rule=Host(`${MAILSERVER_DOMAIN_RAINLOOP?Variable not set}`)&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.mailserver-rainloop-https.entrypoints=https&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.mailserver-rainloop-https.tls=true&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.mailserver-rainloop-https.tls.certresolver=le&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.services.mailserver-rainloop.loadbalancer.server.port=8888&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rainloop:/rainloop/data&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mailserver&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mariadb&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik-public&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I only add the &lt;code&gt;deploy&lt;/code&gt; section and change volumes.&lt;/p&gt;

&lt;h2&gt;
  
  
  MariaDB
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;mariadb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mariadb:10.2&lt;/span&gt;
    &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mariadb.db&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MAILSERVER_RESTART_MODE}&lt;/span&gt;
    &lt;span class="c1"&gt;# Info : These variables are ignored when the volume already exists (if databases was created before).&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MYSQL_RANDOM_ROOT_PASSWORD=yes&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MYSQL_DATABASE=postfix&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MYSQL_USER=postfix&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MYSQL_PASSWORD=${MAILSERVER_DATABASE_USER_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;placement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;constraints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node.role == manager&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sql:/var/lib/mysql&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The MariaDB container gets a &lt;code&gt;placement-constraint&lt;/code&gt; so I don't lose data and docker volumes that are used. Additionally, it is &lt;strong&gt;very important&lt;/strong&gt; that the &lt;code&gt;MYSQL_PASSWORD&lt;/code&gt; is the same as defined within the mailserver-service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redis
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:4.0-alpine&lt;/span&gt;
    &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis.db&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MAILSERVER_RESTART_MODE}&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis-server --appendonly yes&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;placement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;constraints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node.role == manager&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis:/data&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last service is the Redis container which gets a &lt;code&gt;placement-constraint&lt;/code&gt; and adjusted volumes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going Further
&lt;/h2&gt;

&lt;p&gt;After the services were adjusted within one file (&lt;code&gt;docker-compose.mailserver.yml&lt;/code&gt;) the next step was to deploy the Docker Swarm stack. Because I declared several environment variables I had to export them first (and I have to export them every time I recreate the service):&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;export &lt;/span&gt;&lt;span class="nv"&gt;MAILSERVER_DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$PRIMARY_DOMAIN&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MAILSERVER_HOSTNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mail
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MAILSERVER_DOMAIN_POSTFIXADMIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postfixadmin.&lt;span class="nv"&gt;$MAILSERVER_DOMAIN&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MAILSERVER_DOMAIN_RSPAMD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mail-spam.&lt;span class="nv"&gt;$MAILSERVER_DOMAIN&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MAILSERVER_DOMAIN_RAINLOOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;emails.&lt;span class="nv"&gt;$MAILSERVER_DOMAIN&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MAILSERVER_FQDN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$MAILSERVER_HOSTNAME&lt;/span&gt;.&lt;span class="nv"&gt;$MAILSERVER_DOMAIN&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MAILSERVER_DATABASE_USER_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;lkhsaf98oihn53oysf9usaih5nb3rysa98fh3lnoiushdfisdugfbn32thsd9gpiesgs
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MAILSERVER_RSPAMD_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;wsdofytesdf#&lt;span class="k"&gt;*&lt;/span&gt;TYFkjhasd
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MAILSERVER_DOCKER_TAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.1-stable
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MAILSERVER_RESTART_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;unless-stopped
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After exporting these variables I could deploy the mail server to my Docker Swarm by executing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker stack deploy &lt;span class="nt"&gt;-c&lt;/span&gt; docker-compose.mailserver.yml mailserver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the mail server was deployed BUT it does not use SSL. This is because the certificates are generated by traefik the first time &lt;strong&gt;AFTER&lt;/strong&gt; the website was visited for the first time. Because I declared within the mailserver-service that there is a domain called &lt;code&gt;mail.$PRIMARY_DOMAIN&lt;/code&gt;. I opened this website and it showed rspam BUT it also creates the SSL certificate which is needed for the mail server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paulsblog.dev%2Fcontent%2Fimages%2F2022%2F09%2Fimage--24-.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paulsblog.dev%2Fcontent%2Fimages%2F2022%2F09%2Fimage--24-.webp" alt="Mailserver is deployed with Docker within Docker Swarm but need SSL"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How To Use SSL?
&lt;/h2&gt;

&lt;p&gt;For the prior created SSL certificates I need a function to transfer them to the mail server. Unfortunately, the automatic transfer process from the mail server could not be used because I installed the second version of Traefik.&lt;/p&gt;

&lt;p&gt;I had to create a small script that uses dumpcerts (from the mailserver GitHub) for extracting certs from traefik-acme.json and storing them into a file that the mail server can use to have an SSL certificate:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /tmp/dump

./dumpcerts.traefik.v2.sh ./acme.json /tmp/dump
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/dump/certs/&lt;span class="nv"&gt;$MAILSERVER_FQDN&lt;/span&gt;.crt cert/fullchain.pem
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/dump/private/&lt;span class="nv"&gt;$MAILSERVER_FQDN&lt;/span&gt;.key cert/privkey.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script assumes that the &lt;code&gt;acme.json&lt;/code&gt; file that traefik uses to store certificates is located in the same folder as the mail server. The dumpcert script can be found &lt;a href="https://github.com/hardware/mailserver/blob/master/rootfs/usr/local/bin/dumpcerts.traefik.v2.sh?ref=paulsblog.dev" rel="noopener noreferrer"&gt;within the GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If the scripts exit without error the mail server can be turned down and redeployed (because SSL data is only checked at the start of the mail server stack ).&lt;/p&gt;

&lt;p&gt;Additionally, I created an update script that uses all commands together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./update-cert
docker stack &lt;span class="nb"&gt;rm &lt;/span&gt;mailserver
docker stack deploy &lt;span class="nt"&gt;-c&lt;/span&gt; docker-compose.mailserver.yml mailserver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unfortunately, you have to &lt;strong&gt;do this every three months&lt;/strong&gt; because traefik generated certs only have a life span of three months.&lt;/p&gt;

&lt;h2&gt;
  
  
  After install stuff
&lt;/h2&gt;

&lt;p&gt;If everything is started the next task is configuring &lt;code&gt;postfix-admin&lt;/code&gt; and &lt;code&gt;rainloop&lt;/code&gt;. There are two very easy info pages for this where you can find all information about configuring these services: &lt;a href="https://github.com/hardware/mailserver/wiki/Rainloop-initial-configuration?ref=paulsblog.dev" rel="noopener noreferrer"&gt;rainloop-initial&lt;/a&gt;, &lt;a href="https://github.com/hardware/mailserver/wiki/Postfixadmin-initial-configuration?ref=paulsblog.dev" rel="noopener noreferrer"&gt;postfix-initial&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Furthermore, the DNS setup is required so that the mail server works! &lt;strong&gt;This step is very important.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paulsblog.dev%2Fcontent%2Fimages%2F2022%2F09%2Fimage--25-.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paulsblog.dev%2Fcontent%2Fimages%2F2022%2F09%2Fimage--25-.webp" alt="Set important DNS records to have a good score for your mailserver"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Make sure that the &lt;strong&gt;PTR record&lt;/strong&gt; of your IP matches the FQDN (default: mail.domain.tld) of your mail server host. This record is usually set in your web hosting interface.&lt;/li&gt;
&lt;li&gt;  DKIM, SPF, and DMARC records are recommended to build a good reputation score.&lt;/li&gt;
&lt;li&gt;  DMARC record can be created &lt;a href="https://dmarcian.com/dmarc-record-wizard/?ref=paulsblog.dev" rel="noopener noreferrer"&gt;on Dmarcian.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  SPF needs the public IP of the mail server!: &lt;code&gt;v=spf1 a mx ipYOURMAINHERE ~all&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  The DKIM public (&lt;code&gt;mail._domainkey&lt;/code&gt;) key will be available on the host after the container startup here:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/var/lib/docker/volumes/mailserver_mail/_data/dkim/domain.tld/public.key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paulsblog.dev%2Fcontent%2Fimages%2F2022%2F09%2Fimage--26-.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paulsblog.dev%2Fcontent%2Fimages%2F2022%2F09%2Fimage--26-.webp" alt="Docker Swarm mailserver is installed and set up with SSL, PTR record, DKIM, SPF, DMARC"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Notes
&lt;/h2&gt;

&lt;p&gt;Now the mail server is running and you can use postfix admin (postfixadmin.yourdomain.de) to create accounts that can be accessed with Rainloop (webmail.yourdomain.de) or another email client (Thunderbird/Betterbird).&lt;/p&gt;

&lt;p&gt;I hope you find this tutorial helpful and are now able to add a mail server to your Docker Swarm environment.&lt;/p&gt;

&lt;p&gt;Also, if you have any questions, ideas, recommendations, or want to share cool Docker commands or tools, please contact me. I try to answer your question if possible and will test your recommendations.&lt;/p&gt;

&lt;p&gt;This article was originally published on my blog at &lt;a href="https://www.paulsblog.dev/how-to-set-up-a-mailserver-within-a-docker-swarm/" rel="noopener noreferrer"&gt;https://www.paulsblog.dev/how-to-set-up-a-mailserver-within-a-docker-swarm/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to connect with me on &lt;a href="https://www.paulsblog.dev" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;, &lt;a href="https://medium.knulst.de/" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/paulknulst/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://twitter.com/paulknulst" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;, and &lt;a href="https://github.com/paulknulst" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;🙌 Support this content&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you like this content, please consider &lt;a href="https://www.paulsblog.dev/support/" rel="noopener noreferrer"&gt;supporting me&lt;/a&gt;. You can share it on social media or &lt;a href="https://buymeacoffee.com/paulknulst" rel="noopener noreferrer"&gt;buy me a coffee&lt;/a&gt;! Any support helps!&lt;/p&gt;

&lt;p&gt;Furthermore, you can &lt;a href="https://www.paulsblog.dev/#/portal/signup/" rel="noopener noreferrer"&gt;sign up for my newsletter&lt;/a&gt; to show your contribution to my content. See the &lt;a href="https://www.paulsblog.dev/contribute/" rel="noopener noreferrer"&gt;contribute page&lt;/a&gt; for all (free or paid) ways to say thank you!&lt;/p&gt;

&lt;p&gt;Thanks! 🥰&lt;/p&gt;




&lt;p&gt;Photo by &lt;a href="https://www.freepik.com/photos/watching-movie" rel="noopener noreferrer"&gt;Wayhomestudio&lt;/a&gt; / &lt;a href="https://www.freepik.com" rel="noopener noreferrer"&gt;Freepik&lt;/a&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>4 Important Services Everyone Should Deploy In A Docker Swarm</title>
      <dc:creator>Paul Knulst</dc:creator>
      <pubDate>Sat, 29 Jul 2023 08:28:58 +0000</pubDate>
      <link>https://dev.to/paulknulst/4-important-services-everyone-should-deploy-in-a-docker-swarm-1cce</link>
      <guid>https://dev.to/paulknulst/4-important-services-everyone-should-deploy-in-a-docker-swarm-1cce</guid>
      <description>&lt;p&gt;&lt;strong&gt;Enhance your Docker Swarm with four important services that you will love: Traefik, Portainer, Registry, FTP&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In a previous article, I showed &lt;a href="https://www.paulsblog.dev/docker-swarm-in-a-nutshell/"&gt;how a Docker Swarm is set up in ~15 minutes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remember:&lt;/strong&gt; Whenever you read “Docker Swarm” we are talking about “Docker in Docker Swarm mode”&lt;/p&gt;

&lt;p&gt;Within this article, I will show and explain four services everyone should use in their Docker Swarm: &lt;strong&gt;traefik&lt;/strong&gt;, &lt;strong&gt;Portainer&lt;/strong&gt;, &lt;strong&gt;Docker-Registry&lt;/strong&gt;, &lt;strong&gt;FTP&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Traefik
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Makes Networking Boring&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Cloud-Native Networking Stack That Just Works.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One important service in any Docker Swarm is &lt;a href="https://www.traefik.io/?ref=paulsblog.dev"&gt;traefik&lt;/a&gt;. I use this for assigning domains/subdomains to every docker service. A simple docker-compose.yml can be found on my FTP &lt;a href="https://ftp.f1nalboss.de/data/docker-compose.traefik.yml?ref=paulsblog.dev"&gt;here&lt;/a&gt;. To use this file it is necessary to update the labels for your docker swarm by executing:&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;export &lt;/span&gt;&lt;span class="nv"&gt;NODE_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;docker info &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'{{.Swarm.NodeID}}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
docker node update &lt;span class="nt"&gt;--label-add&lt;/span&gt; traefik-public.traefik-public-certificates&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="nv"&gt;$NODE_ID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These two commands assume that you want to have traefik on your manager node! To understand what a manager node &lt;a href="https://docs.docker.com/engine/swarm/how-swarm-mode-works/nodes/?ref=paulsblog.dev"&gt;have a look at the docker documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Another &lt;strong&gt;VERY&lt;/strong&gt; important step is creating a file named &lt;code&gt;acme.json&lt;/code&gt; within the folder where the docker-compose will be stored and do:&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;chmod &lt;/span&gt;600 acme.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Furthermore, you have to define a &lt;code&gt;TRAEFIK_USERNAME&lt;/code&gt;, &lt;code&gt;TRAEFIK_HASHED_PASSWORD&lt;/code&gt;, &lt;code&gt;TRAEFIK_DOMAIN&lt;/code&gt; and &lt;code&gt;TRAEFIK_SSLEMAIL&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TRAEFIK_USERNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;admin
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TRAEFIK_HASHED_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl passwd &lt;span class="nt"&gt;-apr1&lt;/span&gt; testpassword&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TRAEFIK_DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dashboard.YOUR_DOMAIN.tld
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TRAEFIK_SSLEMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_email@address.de
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last command which has to be executed before you can deploy the service is creating the &lt;code&gt;traefik-public&lt;/code&gt; network which will be used across all containers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker network create --driver=overlay traefik-public
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this is done it is possible to deploy traefik to your docker swarm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker stack deploy -c docker-compose.traefik.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just test it by launching &lt;code&gt;https://dashboard.YOUR_DOMAIN.tld&lt;/code&gt;. This domain will use an SSL certificate and you can log in with &lt;strong&gt;&lt;em&gt;admin/testpassword&lt;/em&gt;&lt;/strong&gt; if you followed the above instructions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Portainer
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Portainer is a powerful, GUI-based Container-as-a-Service solution that helps organizations manage and deploy cloud-native applications easily and securely.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My docker-compose.yml for Portainer is not special. You can find it &lt;a href="https://ftp.f1nalboss.de/data/docker-compose.portainer.yml?ref=paulsblog.dev"&gt;here&lt;/a&gt; &lt;strong&gt;if you need it&lt;/strong&gt;. Normally there are no differences between my and the one you find if you google for Portainer service.&lt;/p&gt;

&lt;p&gt;If you use mine you have to *&lt;strong&gt;&lt;em&gt;add a label to your manager!&lt;/em&gt;&lt;/strong&gt;* *&lt;strong&gt;&lt;em&gt;This is very important&lt;/em&gt;&lt;/strong&gt;* because Portainer needs a connection to your docker socket. You can add the label with these commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;NODE_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;docker info &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'{{.Swarm.NodeID}}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
docker node update &lt;span class="nt"&gt;--label-add&lt;/span&gt; portainer.portainer-data&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="nv"&gt;$NODE_ID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Furthermore declare the &lt;code&gt;PORTAINER_DOMAIN&lt;/code&gt; which will be used.&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;export &lt;/span&gt;&lt;span class="nv"&gt;PORTAINER_DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;portainer.&lt;span class="nv"&gt;$PRIMARY_DOMAIN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After you have done this you can deploy Portainer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker stack deploy &lt;span class="nt"&gt;-c&lt;/span&gt; docker-compose.portainer.yml portainer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Timing Note:&lt;/strong&gt; Make sure you log in and create your credentials soon after Portainer is ready, or it will automatically shut down itself for security. If you didn’t create the credentials on time and it shut down itself automatically, you can force it to restart with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker service update portainer_portainer &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Docker Registry
&lt;/h2&gt;

&lt;p&gt;The Registry is a stateless, highly scalable server side application that stores and lets you distribute Docker images. The Registry is open-source, under the permissive Apache license.&lt;/p&gt;

&lt;p&gt;The docker registry is important to have in a swarm environment if you don't want to upload your code/data to a public registry. If you don't have any private registry for yourself you have to upload the resulting image to docker-hub each time you extend an official image with your application code. While this is great if you only want to extend functionality in a general way it is not advisable if you copy your website with your closed source code into the image.&lt;/p&gt;

&lt;p&gt;Imagine you have a modified Nginx image where the HTML folder is copied into the image:&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; nginx&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; html /usr/share/nginx/html&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In many cases the &lt;code&gt;html&lt;/code&gt; folder will contain your website which you don't want someone to use. If you have a private registry you can build the image and upload it but if you don't have one you have to use a public registry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's why I think that every swarm needs to have access to a private registry.&lt;/strong&gt; And that's why I created one. Normally you can set up a registry with a simple &lt;code&gt;docker run&lt;/code&gt; command but I wanted to have a registry that can be reached from everywhere. Because of that, I created a registry that could be accessed by username and password.&lt;/p&gt;

&lt;p&gt;My personal &lt;code&gt;docker-compose.yml&lt;/code&gt; can be downloaded &lt;a href="https://ftp.f1nalboss.de/data/docker-compose.registry.yml?ref=paulsblog.dev"&gt;here&lt;/a&gt;. It will run within my Traefik environment and create a new registry that I can use from every node in my swarm. To deploy the service you have to declare some environment variables which are used while deploying: &lt;code&gt;REGISTRY_USERNAME&lt;/code&gt;, &lt;code&gt;REGISTRY_HASHED_PASSWORD&lt;/code&gt; and &lt;code&gt;REGISTRY_HOST&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;REGISTRY_USERNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;reg_adm
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;REGISTRY_HASHED_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl passwd &lt;span class="nt"&gt;-apr1&lt;/span&gt; regsupersecret&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;REGISTRY_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;reg.YOUR_DOMAIN.tld
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With these variables you can deploy the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker stack deploy &lt;span class="nt"&gt;-c&lt;/span&gt; docker-compose.registry.yml registry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To use the registry you have to do two more things. The first thing is a quality of life feature to easily change the domain of the registry without affecting every container which uses an image from the private registry. *&lt;strong&gt;&lt;em&gt;Add DOCKER_REGISTRY to .profile for&lt;/em&gt;&lt;/strong&gt;* &lt;code&gt;****root****&lt;/code&gt; *&lt;strong&gt;&lt;em&gt;or whatever user you are using so it is known if docker-compose files should be pushed/downloaded&lt;/em&gt;&lt;/strong&gt;*. The second task is very important. You have to execute &lt;code&gt;docker login&lt;/code&gt; on every node of your swarm so that every node is allowed to pull images.&lt;/p&gt;

&lt;p&gt;Now you can start using your private registry in docker-compose.yml. If you created a &lt;code&gt;DOCKER_REGISTRY&lt;/code&gt; environment variable you can use it like this in your docker-compose.yml:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;myapp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DOCKER_REGISTRY}/simple-app&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to deploy a service that contains the above part you have to build and push your image to deploy it correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose build
docker-compose push
docker stack deploy &lt;span class="nt"&gt;-c&lt;/span&gt; docker-compose.yml www
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you only build and deploy it other nodes in your swarm cannot pull the image and so the service cannot be deployed.&lt;/p&gt;

&lt;h2&gt;
  
  
  FTP
&lt;/h2&gt;

&lt;p&gt;Another important service is an FTP server for saving files you want to use anywhere in any app or service you create while working with the swarm.&lt;/p&gt;

&lt;p&gt;I decided to use “pure-ftp” because it was the one I found while googling for a nice FTP server that runs within a docker environment. Because I want to keep it simple I created an FTP server without any &lt;code&gt;traefik&lt;/code&gt; configuration. &lt;strong&gt;BUT&lt;/strong&gt; I did a simple trick in putting the FTP server together with a website in a docker-compose.yml. I have done this because I want to have the possibility to download files from the server over &lt;code&gt;https&lt;/code&gt; which I uploaded with &lt;code&gt;ftp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After configuration, I came up with &lt;a href="https://ftp.f1nalboss.de/data/docker-compose.web.yml?ref=paulsblog.dev"&gt;this docker-compose.yml&lt;/a&gt;. Within the file, you can see that I use a custom Dockerfile for the web service which contains:&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; nginx&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; html /usr/share/nginx/html&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trick I describe is just the defined volume within the &lt;code&gt;docker-compose.yml&lt;/code&gt;. As you can see I defined data in both services. Within &lt;code&gt;web-service&lt;/code&gt; I just have an extra folder within the Nginx HTML folder so that I can access it from the web. And within the &lt;code&gt;ftp-service&lt;/code&gt; I define data as the place where user can upload their data.&lt;/p&gt;

&lt;p&gt;Before it is possible to deploy this service you have to declare environment variables: &lt;code&gt;FTP_USERNAME&lt;/code&gt;, &lt;code&gt;FTP_PASSWORD&lt;/code&gt;, &lt;code&gt;FTP_DOMAIN_FOR_CERT&lt;/code&gt;, &lt;code&gt;FTP_ORG_FOR_CERT&lt;/code&gt;, &lt;code&gt;FTP_COUNTRYCODE_FOR_CERT&lt;/code&gt; and &lt;code&gt;WEBSERVICE_DOMAIN&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;WEBSERVICE_DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;www.MYDOMAIN.tld
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;FTP_USERNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;SUPERUSER
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;FTP_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;clearTextPW
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;FTP_DOMAIN_FOR_CERT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$WEBSERVICE_DOMAIN&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;FTP_ORG_FOR_CERT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mybusiness
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;FTP_COUNTRYCODE_FOR_CERT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Furthermore, you have to add a label to any node of your swarm. To achieve this use &lt;code&gt;docker node ls&lt;/code&gt; to find out the ID from every node and execute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker node update &lt;span class="nt"&gt;--label-add&lt;/span&gt; www.ftp-data&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;ID_OF_NODE_TO_USE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this is done you can safely deploy your website with enabled FTP&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker stack deploy &lt;span class="nt"&gt;-c&lt;/span&gt; docker-compose.web.yml webandftp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now it is possible to connect with an &lt;a href="https://filezilla-project.org/?ref=paulsblog.dev"&gt;FTP client&lt;/a&gt; to your &lt;code&gt;WEBSERVICE_DOMAIN&lt;/code&gt; and upload a file (&lt;code&gt;test.txt&lt;/code&gt;) which then can be accessed by this URL: &lt;code&gt;WEBSERVICE_DOMAIN/data/test.txt&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The whole FTP docker service can be downloaded from my GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/paulscode-de/ftp-server"&gt;https://github.com/paulscode-de/ftp-server&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is very important to say that you only can connect with your WEBSERVICE_DOMAIN if this domain also has an A record to your manager node!&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Closing Notes
&lt;/h2&gt;

&lt;p&gt;I hope you find this article helpful and can use my provided files to set up these services within your own Docker Swarm.&lt;/p&gt;

&lt;p&gt;In my humble opinion, these four services should be present in every Docker Swarm environment because they are mandatory (or exchanged with function-like services).&lt;/p&gt;

&lt;p&gt;This article was originally published on my blog at &lt;a href="https://www.paulsblog.dev/services-you-want-to-have-in-a-swarm-environment/"&gt;https://www.paulsblog.dev/services-you-want-to-have-in-a-swarm-environment/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to connect with me on &lt;a href="https://www.paulsblog.dev"&gt;my blog&lt;/a&gt;, &lt;a href="https://medium.knulst.de/"&gt;Medium&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/paulknulst/"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://twitter.com/paulknulst"&gt;Twitter&lt;/a&gt;, and &lt;a href="https://github.com/paulknulst"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;🙌 Support this content&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you like this content, please consider &lt;a href="https://www.paulsblog.dev/support/"&gt;supporting me&lt;/a&gt;. You can share it on social media or &lt;a href="https://buymeacoffee.com/paulknulst"&gt;buy me a coffee&lt;/a&gt;! Any support helps!&lt;/p&gt;

&lt;p&gt;Furthermore, you can &lt;a href="https://www.paulsblog.dev/#/portal/signup/"&gt;sign up for my newsletter&lt;/a&gt; to show your contribution to my content. See the &lt;a href="https://www.paulsblog.dev/contribute/"&gt;contribute page&lt;/a&gt; for all (free or paid) ways to say thank you!&lt;/p&gt;

&lt;p&gt;Thanks! 🥰&lt;/p&gt;




&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@frankiefoto?utm_source=ghost&amp;amp;utm_medium=referral&amp;amp;utm_campaign=api-credit"&gt;frank mckenna&lt;/a&gt; / &lt;a href="https://unsplash.com/?utm_source=ghost&amp;amp;utm_medium=referral&amp;amp;utm_campaign=api-credit"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>devops</category>
      <category>beginners</category>
      <category>docker</category>
    </item>
    <item>
      <title>Deploy Free Figma Alternative Penpot With Docker</title>
      <dc:creator>Paul Knulst</dc:creator>
      <pubDate>Tue, 06 Jun 2023 11:58:49 +0000</pubDate>
      <link>https://dev.to/paulknulst/deploy-free-figma-alternative-penpot-with-docker-2nkn</link>
      <guid>https://dev.to/paulknulst/deploy-free-figma-alternative-penpot-with-docker-2nkn</guid>
      <description>&lt;p&gt;&lt;a href="https://www.paulsblog.dev/" rel="noopener noreferrer"&gt;Paul Knulst&lt;/a&gt;  in  &lt;a href="https://www.paulsblog.dev/tag/docker/" rel="noopener noreferrer"&gt;Docker&lt;/a&gt; • Oct 6, 2022 • 8 min read&lt;/p&gt;




&lt;p&gt;As we all probably heard Adobe has acquired the popular design tool &lt;a href="https://www.figma.com/" rel="noopener noreferrer"&gt;Figma&lt;/a&gt; for a whopping &lt;strong&gt;$20 billion&lt;/strong&gt;. Unfortunately, this "strategy" of eliminating competition by acquiring businesses is common for big tech companies.&lt;/p&gt;

&lt;p&gt;But, &lt;strong&gt;luckily&lt;/strong&gt;, there is a &lt;strong&gt;free and open-source&lt;/strong&gt; design tool that also does some things better. Also, it was inspired by Figma.&lt;/p&gt;

&lt;h2&gt;
  
  
  Penpot: Free &amp;amp; Open-Source Design Tool
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://penpot.app/" rel="noopener noreferrer"&gt;Penpot&lt;/a&gt; is an open-source project that is actively developed and totally free to use for everyone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here are some major features that make Penpot interesting&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Free and open-source (of course).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Option to Self-host (will be covered here primarily).&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  Cross-platform.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Using SVG&lt;/strong&gt; as the native format.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Web-based&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;  Featuring industry-standard features (inspired by Figma).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Watch this official Penpot video to learn about the basics of Penpot:&lt;br&gt;
&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/JozESuPcVpg"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;A very important feature of Penpot is the use of SVG as their native format instead of PNG/JPG because it enables you to be compatible with many vector graphic editing tools.&lt;/p&gt;

&lt;p&gt;This is especially useful because you will not get locked by being forced to use a proprietary file format that no other application can use.&lt;/p&gt;

&lt;p&gt;Also, Penpot aims to use the absolute best of open standards that already exist. The &lt;strong&gt;CEO of Penpot,&lt;/strong&gt; &lt;em&gt;Pablo Ruiz-Múzquiz&lt;/em&gt;, mentions more about it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you go for SVG (open standard, web, mobile, etc) at the storage level, you can suddenly integrate all your Penpot designs with your code repos. You could make changes to the actual representation of the design itself thanks to SVG and not yet another closed format. That opens the door to massive opportunities for designers AND devs. Also, SVG means we are low-code ready for free. You can pick any element in Penpot and ask for its SVG (and CSS) representation knowing it's actually what it is, no translation. That brings a more trustworthy relationship between designers and devs and allows frontend devs to try out their design skillset.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;As mentioned before a very important feature of Penpot is the &lt;strong&gt;ability to self-host&lt;/strong&gt; it in a Docker container on your local machine and also on any server (that runs Docker). The following steps will show how easily you can use Docker to host your own Penpot to replace Figma as your design tool.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you want to test Penpot before installing it as a self-hosted version you can go to &lt;a href="https://design.penpot.app/" rel="noopener noreferrer"&gt;https://design.penpot.app/&lt;/a&gt; log in with GitHub/Gitlab/Google account or create n new account to test it. But I would avoid doing this and sharing your personal data because deploying it locally is done super fast (if you already have Docker installed)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy Penpot locally with Docker
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install Docker
&lt;/h3&gt;

&lt;p&gt;To deploy Penpot locally (or remotely) you need to have Docker installed. To install Docker on your system follow the &lt;a href="https://docs.docker.com/get-docker/" rel="noopener noreferrer"&gt;official tutorial on docker.com&lt;/a&gt;. If you are using Windows and &lt;strong&gt;aren't allowed to install Docker Desktop&lt;/strong&gt; you can follow this guide:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.paulsblog.dev/how-to-install-docker-without-docker-desktop-on-windows/" rel="noopener noreferrer"&gt;How To Install Docker Without Docker Desktop On Windows&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy Penpot
&lt;/h3&gt;

&lt;p&gt;If Docker is installed it is very easy to start up a Penpot instance locally. To do this you can simply retrieve the latest Compose and config file from &lt;a href="https://github.com/penpot/penpot" rel="noopener noreferrer"&gt;the official Penpot Github project&lt;/a&gt; by download them:&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://raw.githubusercontent.com/penpot/penpot/main/docker/images/docker-compose.yaml
    wget https://raw.githubusercontent.com/penpot/penpot/main/docker/images/config.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The config file from the GitHub project is quite big and contains several variables that you will never use working locally with Penpot. It would be sufficient to use the following config as it only contains mandatory values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;    &lt;span class="c"&gt;# Standard database connection parameters (only postgresql is supported):&lt;/span&gt;
    &lt;span class="nv"&gt;PENPOT_DATABASE_URI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgresql://penpot-postgres/penpot
    &lt;span class="nv"&gt;PENPOT_DATABASE_USERNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;penpot
    &lt;span class="nv"&gt;PENPOT_DATABASE_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;penpot

    &lt;span class="c"&gt;# Redis is used for the websockets notifications.&lt;/span&gt;
    &lt;span class="nv"&gt;PENPOT_REDIS_URI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;redis://penpot-redis/0

    &lt;span class="nv"&gt;ASSETS_STORAGE_BACKEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;assets-fs
    &lt;span class="nv"&gt;PENPOT_STORAGE_ASSETS_FS_DIRECTORY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/opt/data/assets

    &lt;span class="nv"&gt;PENPOT_TELEMETRY_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;

    &lt;span class="c"&gt;# Enable or disable external user registration process.&lt;/span&gt;
    &lt;span class="nv"&gt;PENPOT_REGISTRATION_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that you downloaded (or created) both files switch to the folder and start the Docker service with:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;If the process finishes you can open &lt;a href="http://localhost:9001" rel="noopener noreferrer"&gt;http://localhost:9001&lt;/a&gt; and will see the following screen if Penpot was installed correctly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.paulsblog.dev%2Fcontent%2Fimages%2F2022%2F10%2Fimage--6-.webp" alt="Deploy Free Figma Alternative Penpot With Docker and Traefik Locally And on a server"&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;center&gt;&lt;em&gt;Login screen for Penpot after deploying it to localhost with Docker&lt;/em&gt;&lt;/center&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Create a Penpot user
&lt;/h3&gt;

&lt;p&gt;Although you deployed Penpot on your local machine you cannot create a user using the Web interface because you started Penpot without configuring an SMTP account to receive registration emails. However, this is no problem because you can simply use this Docker command to manually create an already-activated user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;    docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; penpot-penpot-backend-1 ./manage.sh create-profile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;IMPORTANT&lt;/strong&gt;: Check if &lt;code&gt;penpot-penpot-backend-1&lt;/code&gt; is the correct name of your backend container. You can do this by executing &lt;code&gt;docker ps&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After executing the command you could get the following output which is confusing:&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="o"&gt;[&lt;/span&gt;2022-10-06 10:18:13.523] I app.config - &lt;span class="nv"&gt;hint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"flags initialized"&lt;/span&gt;, &lt;span class="nv"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"backend-api-doc,secure-session-cookies,login,backend-worker,registration"&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt;2022-10-06 10:18:14.295] I app.metrics - &lt;span class="nv"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"initialize metrics"&lt;/span&gt;
    Email:  &lt;span class="o"&gt;[&lt;/span&gt;2022-10-06 10:18:14.334] I app.db - &lt;span class="nv"&gt;hint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"initialize connection pool"&lt;/span&gt;, &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"main"&lt;/span&gt;, &lt;span class="nv"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"postgresql://penpot-postgres/penpot"&lt;/span&gt;, read-only&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;, with-credentials&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;, min-size&lt;span class="o"&gt;=&lt;/span&gt;0, max-size
    &lt;span class="o"&gt;=&lt;/span&gt;30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ignore everything and just type an email that you want to use in your local installation, then set your name and password. The resulting log will look like this:&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="o"&gt;[&lt;/span&gt;2022-10-06 10:22:33.900] I app.config - &lt;span class="nv"&gt;hint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"flags initialized"&lt;/span&gt;, &lt;span class="nv"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"backend-api-doc,secure-session-cookies,login,backend-worker,registration"&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt;2022-10-06 10:22:34.636] I app.metrics - &lt;span class="nv"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"initialize metrics"&lt;/span&gt;
    Email:  &lt;span class="o"&gt;[&lt;/span&gt;2022-10-06 10:22:34.675] I app.db - &lt;span class="nv"&gt;hint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"initialize connection pool"&lt;/span&gt;, &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"main"&lt;/span&gt;, &lt;span class="nv"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"postgresql://penpot-postgres/penpot"&lt;/span&gt;, read-only&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;, with-credentials&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;, min-size&lt;span class="o"&gt;=&lt;/span&gt;0, max-size
    &lt;span class="o"&gt;=&lt;/span&gt;30
    user@local.de
    Full Name:  Paul Knulst
    Password:
    User created successfully.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After your user was created you can log in to your Penpot installation at &lt;a href="http://localhost:9001" rel="noopener noreferrer"&gt;http://localhost:9001/&lt;/a&gt; and start using Penpot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy Penpot remotely with Docker and Traefik
&lt;/h2&gt;

&lt;p&gt;Having a running installation of Penpot locally is very useful if you are the only person working with it. Normally, you will work in teams where multiple designers, developers, or others collaborate together on different designs. To allow doing this with Penpot you can deploy Penpot on any server using Docker and Traefik.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing Traefik
&lt;/h3&gt;

&lt;p&gt;To install Traefik on your server and use it to forward URLs to Docker service and automatically assign SSL certificates you can follow this guide:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.paulsblog.dev/how-to-setup-traefik-with-automatic-letsencrypt-certificate-resolver/" rel="noopener noreferrer"&gt;How to setup Traefik v2 with automatic Let’s Encrypt certificate resolver&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Keep in mind that you need at least one URL that points to the server where your Traefik instance will run. If you do not have any URL you have to buy one and map it to your server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Updating Config
&lt;/h3&gt;

&lt;p&gt;Deploying Penpot on a server with a specific URL where multiple users can work together needs some changes to the prior shown config file because the registration process should be enabled. Therefore, you have to add an email account that will be used by Penpot to send registration emails. A simple Google/GMX/Apple Mail account will be sufficient.&lt;/p&gt;

&lt;p&gt;Change the &lt;code&gt;PENPOT_REGISTRATION_ENABLED&lt;/code&gt; environment variable to true and add the following variables (with correct values) to the config 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="nv"&gt;PENPOT_REGISTRATION_DOMAIN_WHITELIST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;

    &lt;span class="c"&gt;#Your public penpot url&lt;/span&gt;
    &lt;span class="nv"&gt;PENPOT_PUBLIC_URI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;

    &lt;span class="c"&gt;# Enable Email &lt;/span&gt;
    &lt;span class="nv"&gt;PENPOT_SMTP_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
    &lt;/span&gt;&lt;span class="nv"&gt;PENPOT_SMTP_DEFAULT_FROM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nv"&gt;PENPOT_SMTP_DEFAULT_REPLY_TO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nv"&gt;PENPOT_SMTP_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nv"&gt;PENPOT_SMTP_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nv"&gt;PENPOT_SMTP_USERNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nv"&gt;PENPOT_SMTP_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nv"&gt;PENPOT_SMTP_TLS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nv"&gt;PENPOT_SMTP_SSL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An important setting here is &lt;code&gt;PENPOT_REGISTRATION_DOMAIN_WHITELIST=""&lt;/code&gt; where you can add a comma-separated list of allowed email domains to register. If you leave it empty every domain is allowed. &lt;a href="https://gist.github.com/paulknulst/18829ae330bf6fcd3917aee7ff4a3b7d" rel="noopener noreferrer"&gt;Find the full config in this gist on Github&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adjust Compose file for Traefik usage
&lt;/h3&gt;

&lt;p&gt;After updating the config file you need to update the Compose file and add the Traefik network and the labels for Traefik to automatically generate an SSL certificate after deploying Penpot. Copy the content of the following Compose file into yours:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.5"&lt;/span&gt;

    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="na"&gt;traefik-public&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;assets_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

    &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;frontend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;penpotapp/frontend:latest"&lt;/span&gt;
        &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;frontend.penpot&lt;/span&gt;
        &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;assets_data:/opt/data&lt;/span&gt;
        &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;config.env&lt;/span&gt;
        &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;penpot-backend&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;penpot-exporter&lt;/span&gt;
        &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik-public&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.enable=true&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.docker.network=traefik-public&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.constraint-label=traefik-public&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.penpot-http.rule=Host(`${PENPOT_URL}`)&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.penpot-http.entrypoints=http&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.penpot-http.middlewares=https-redirect&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.penpot-https.rule=Host(`${PENPOT_URL}`)&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.penpot-https.entrypoints=https&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.penpot-https.tls=true&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.penpot-https.tls.certresolver=le&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.services.penpot.loadbalancer.server.port=80&lt;/span&gt;
      &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;penpotapp/backend:latest"&lt;/span&gt;
        &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backend.penpot&lt;/span&gt;
        &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;assets_data:/opt/data&lt;/span&gt;
        &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;
        &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;config.env&lt;/span&gt;
        &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;

      &lt;span class="na"&gt;exporter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;penpotapp/exporter:latest"&lt;/span&gt;
        &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="c1"&gt;# Don't touch it; this uses internal docker network to&lt;/span&gt;
          &lt;span class="c1"&gt;# communicate with the frontend.&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PENPOT_PUBLIC_URI=http://frontend.penpot&lt;/span&gt;
        &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;

      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres:13"&lt;/span&gt;
        &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
        &lt;span class="na"&gt;stop_signal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SIGINT&lt;/span&gt;
        &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_INITDB_ARGS=--data-checksums&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_DB=penpot&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_USER=penpot&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_PASSWORD=penpot&lt;/span&gt;
        &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres_data:/var/lib/postgresql/data&lt;/span&gt;
        &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;

      &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:6&lt;/span&gt;
        &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
        &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Deploy Penpot
&lt;/h3&gt;

&lt;p&gt;Before you finally can deploy the Docker service and use Penpot with your coworkers/friends you have to set your URL. This has to be the same URL that you used in the config (without HTTPS). Also, the URL has to point to your server!&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;export &lt;/span&gt;&lt;span class="nv"&gt;PENPOT_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-website.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, switch to your folder and deploy your Penpot service:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Switch to your URL and register a new user. You should get an email from Penpot and can use it with your coworkers.&lt;/p&gt;

&lt;p&gt;Keep in mind that you still can deactivate registration by setting the appropriate environment variable within the config and redeploying the server. If you do this you can create new users by executing the following command and manually set username/password:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;    docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; penpot-penpot-backend-1 ./manage.sh create-profile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this approach, you will not need an email account and still be able to work with others.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Notes
&lt;/h2&gt;

&lt;p&gt;Congratulations if you followed my approach you have just installed Penpot as your own Figma replacement on either your local machine or on a server to use with collaborators.&lt;/p&gt;

&lt;p&gt;The full Compose file (and config) for remote deployment can be found &lt;a href="https://gist.github.com/paulknulst/18829ae330bf6fcd3917aee7ff4a3b7d" rel="noopener noreferrer"&gt;within the GitHub Gist&lt;/a&gt; I created for this article.&lt;/p&gt;

&lt;p&gt;This is the end of this tutorial. Hopefully, you are now able to set up your personal installation. If you still have questions about setting up Penpot as a replacement for Figma you can just ask in the comment section. Also, if you enjoyed reading this article consider commenting with your valuable thoughts! I would love to hear your feedback about my tutorial or Penpot in general.&lt;/p&gt;

&lt;p&gt;Furthermore, share this article with fellow designers to help them to replace Figma with Penpot.&lt;/p&gt;

&lt;p&gt;This article was originally published on my blog at &lt;a href="https://www.paulsblog.dev/deploy-free-figma-alternative-penpot-with-docker/" rel="noopener noreferrer"&gt;https://www.paulsblog.dev/deploy-free-figma-alternative-penpot-with-docker/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to connect with me on &lt;a href="https://www.paulsblog.dev" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;, &lt;a href="https://medium.knulst.de/" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/paulknulst/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://twitter.com/paulknulst" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;, and &lt;a href="https://github.com/paulknulst" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;🙌 Support this content&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you like this content, please consider &lt;a href="https://www.paulsblog.dev/support/" rel="noopener noreferrer"&gt;supporting me&lt;/a&gt;. You can share it on social media or &lt;a href="https://buymeacoffee.com/paulknulst" rel="noopener noreferrer"&gt;buy me a coffee&lt;/a&gt;! Any support helps!&lt;/p&gt;

&lt;p&gt;Furthermore, you can &lt;a href="https://www.paulsblog.dev/#/portal/signup/" rel="noopener noreferrer"&gt;sign up for my newsletter&lt;/a&gt; to show your contribution to my content. See the &lt;a href="https://www.paulsblog.dev/contribute/" rel="noopener noreferrer"&gt;contribute page&lt;/a&gt; for all (free or paid) ways to say thank you!&lt;/p&gt;

&lt;p&gt;Thanks! 🥰&lt;/p&gt;




&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@theshubhamdhage?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;GuerrillaBuzz Crypto PR&lt;/a&gt; / &lt;a href="https://unsplash.com/s/photos/figma?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Optimize Android App Development With Docker, SonarQube, Detekt, and MobSF</title>
      <dc:creator>Paul Knulst</dc:creator>
      <pubDate>Mon, 08 May 2023 09:03:55 +0000</pubDate>
      <link>https://dev.to/paulknulst/optimize-android-app-development-with-docker-sonarqube-detekt-and-mobsf-12f0</link>
      <guid>https://dev.to/paulknulst/optimize-android-app-development-with-docker-sonarqube-detekt-and-mobsf-12f0</guid>
      <description>&lt;p&gt;&lt;strong&gt;Improve the Security and Code Quality of Android apps by using SonarQube, Detekt, and MobSF combined in the Android software-quality-chain!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.paulsblog.dev/author/paulknulst/"&gt;Paul Knulst&lt;/a&gt;  in  &lt;a href="https://www.paulsblog.dev/tag/android-app-development/"&gt;Android App Development&lt;/a&gt; • 13 min read&lt;/p&gt;




&lt;p&gt;As an Android app developer, you should always want to implement the best code that is possible. Also, your app should be as secure as possible.&lt;/p&gt;

&lt;p&gt;Code quality is very important for any kind of software and you should always try to optimize your implementation. This article shows and explains different quality tools that will be combined in an Android &lt;strong&gt;software-quality-chain&lt;/strong&gt; to achieve better code quality, a more secure app, and improved maintainability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;tl;dr:&lt;/strong&gt; &lt;em&gt;&lt;a href="https://github.com/paulknulst/android-software-quality-chain?ref=paulsblog.dev"&gt;Clone this GitHub repository&lt;/a&gt; into your Android project folder and execute the full chain with: &lt;code&gt;sh software-quality-chain.sh&lt;/code&gt;. Afterward, open the URL that will be logged in to the console. Log in with admin:admin12345 and see your Android project report.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Docker
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;To run the script Docker needs to be installed on your operating system&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Docker is a widely used platform for developing, shipping, and running all kinds of applications. It enables you to separate infrastructure from your applications to quickly deliver software from one machine to another.&lt;/p&gt;

&lt;p&gt;Implementing software while using Docker often negates infrastructure problems like the common "works on my machine" problem:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7-Y0s7tJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.paulsblog.dev/content/images/2022/10/image.png" alt="Use Docker To Optimize Android App Development With SonarQube, Detekt, and MobSF to avoid using server errors" width="568" height="426"&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;center&gt;&lt;em&gt;Created with memegenerator - Disaster Kid&lt;/em&gt;&lt;/center&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To install Docker on your system follow the &lt;a href="https://docs.docker.com/get-docker/?ref=paulsblog.dev"&gt;official tutorial on docker.com&lt;/a&gt;. If you are using Windows and &lt;strong&gt;aren't allowed to install Docker Desktop&lt;/strong&gt; you can follow this guide:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.paulsblog.dev/how-to-install-docker-without-docker-desktop-on-windows/"&gt;https://www.paulsblog.dev/how-to-install-docker-without-docker-desktop-on-windows/&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Detekt
&lt;/h3&gt;

&lt;p&gt;Detekt is&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;__&lt;strong&gt;&lt;em&gt;a static code analysis tool for the Kotlin programming language. It operates on the abstract syntax tree provided by the Kotlin compiler.&lt;/em&gt;&lt;/strong&gt;__&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It can be implemented in any Kotlin-based Android app and can be used with a self-defined set of rules to check an app. Detekt is a mandatory prerequisite to executing the script and has to be installed within your Android App. Fortunately, this procedure is very easy and can be done in four simple steps:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Add Detekt to project&lt;/strong&gt; &lt;code&gt;build.gradle&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gradle"&gt;&lt;code&gt;&lt;span class="k"&gt;dependencies&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; 
    &lt;span class="o"&gt;[...]&lt;/span&gt;
    &lt;span class="n"&gt;classpath&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.17.0"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Create a detekt.yml with a defined set of rules.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Download a sample &lt;a href="https://ftp.f1nalboss.de/data/sample-detekt.yml?ref=paulsblog.dev"&gt;gradle detek.yml file here&lt;/a&gt; and store it within &lt;code&gt;project_folder/detekt/detekt.yml&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Apply the detekt plugin in app&lt;/strong&gt; &lt;code&gt;build.gradle&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gradle"&gt;&lt;code&gt;&lt;span class="n"&gt;apply&lt;/span&gt; &lt;span class="nl"&gt;plugin:&lt;/span&gt; &lt;span class="s2"&gt;"io.gitlab.arturbosch.detekt"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Add detekt block to app&lt;/strong&gt; &lt;code&gt;build.gradle&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gradle"&gt;&lt;code&gt;&lt;span class="n"&gt;detekt&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;toolVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1.17.0"&lt;/span&gt;
    &lt;span class="n"&gt;buildUponDefaultConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="n"&gt;allRules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"../detekt/detekt.yml"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;baseline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"../detekt/baseline.xml"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"src/main/java/com"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;debug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="n"&gt;reports&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
            &lt;span class="n"&gt;destination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"build/reports/detekt.html"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;xml&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
            &lt;span class="n"&gt;destination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"build/reports/detekt.xml"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;txt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="n"&gt;sarif&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Other Tools Used
&lt;/h2&gt;

&lt;h3&gt;
  
  
  MobSF
&lt;/h3&gt;

&lt;p&gt;Mobile Security Framework (MobSF)&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;is an automated, all-in-one mobile application (Android/iOS/Windows) pen-testing, malware analysis and security assessment framework capable of performing static and dynamic analysis.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Before continuing, it is necessary to understand mobile security and how to create an android app in a secure way. Luckily, &lt;a href="https://owasp.org/?ref=paulsblog.dev"&gt;OWASP&lt;/a&gt; (Open Web Application Security Project), a popular nonprofit organization that is known for community-led open-source software projects and for improving software security created the "OWASP Mobile Top Ten" to improve mobile security. These include the most critical security issues that should be avoided in any Android app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://owasp.org/www-project-mobile-top-10/2016-risks/m1-improper-platform-usage?ref=paulsblog.dev"&gt;M1: Improper Platform Usage&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://owasp.org/www-project-mobile-top-10/2016-risks/m2-insecure-data-storage?ref=paulsblog.dev"&gt;M2: Insecure Data Storage&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://owasp.org/www-project-mobile-top-10/2016-risks/m3-insecure-communication?ref=paulsblog.dev"&gt;M3: Insecure Communication&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://owasp.org/www-project-mobile-top-10/2016-risks/m4-insecure-authentication?ref=paulsblog.dev"&gt;M4: Insecure Authentication&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://owasp.org/www-project-mobile-top-10/2016-risks/m5-insufficient-cryptography?ref=paulsblog.dev"&gt;M5: Insufficient Cryptography&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://owasp.org/www-project-mobile-top-10/2016-risks/m6-insecure-authorization?ref=paulsblog.dev"&gt;M6: Insecure Authorization&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://owasp.org/www-project-mobile-top-10/2016-risks/m7-client-code-quality?ref=paulsblog.dev"&gt;M7: Client Code Quality&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://owasp.org/www-project-mobile-top-10/2016-risks/m8-code-tampering?ref=paulsblog.dev"&gt;M8: Code Tampering&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://owasp.org/www-project-mobile-top-10/2016-risks/m9-reverse-engineering?ref=paulsblog.dev"&gt;M9: Reverse Engineering&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://owasp.org/www-project-mobile-top-10/2016-risks/m10-extraneous-functionality?ref=paulsblog.dev"&gt;M10: Extraneous Functionality&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fortunately, MobSF can help you to identify many possible security issues in your Android app in an automated way. The security framework works best for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  local environment&lt;/li&gt;
&lt;li&gt;  performing a quick security test&lt;/li&gt;
&lt;li&gt;  implemented in CI/CD with mobsfscan (will be used here)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To test MobSF you can switch to &lt;a href="https://mobsf.live/?ref=paulsblog.dev"&gt;https://mobsf.live&lt;/a&gt; and upload any Android APK that will then be analyzed for common security issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My advice:&lt;/strong&gt; all developers should use MobSF to identify many security vulnerabilities during development by doing a static code analysis. This analysis will check the source code without running the application. It can be used during mobile application development and should be carried out regularly. Normally, it should be executed before every app release, update and Pull Request submission.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep in mind that using MobSF for static code analysis does not guarantee that your mobile application is safe! But it will help to identify the most obvious security flaws.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  SonarQube
&lt;/h3&gt;

&lt;p&gt;SonarQube&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;__&lt;strong&gt;&lt;em&gt;is an open-source platform developed by SonarSource for continuous inspection of code quality to perform automatic reviews with static analysis of code to detect bugs, code smells, and security vulnerabilities on 20+ programming languages.&lt;/em&gt;&lt;/strong&gt;__&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For your projects, SonarQube analyzes source code (not only Android/Kotlin apps), evaluates the quality, and generates reports. It allows for the continuous monitoring of quality throughout time and combines static and dynamic analytic methods. SonarQube inspects and evaluates everything that has an impact on our code base, from inconspicuous styling decisions to serious design flaws.&lt;/p&gt;

&lt;p&gt;As a result, developers can access and track code analysis information on everything from styling mistakes, potential bugs, and code defects to design inefficiencies, code duplication, inadequate test coverage, and excessive complexity. The Sonar platform examines source code from a variety of angles; as a result, it digs down to your code layer by layer, progressing from the module level to the class level. Additionally, it generates metrics and data at each level, highlighting problematic regions in the source that need to be examined or improved. It automatically detects what type of software you use: Kotlin-based Android app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Other Features:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
SonarQube does more than merely highlight issues. It also provides quality-management tools to proactively assist you in making corrections.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  SonarQube provides information on a dashboard on code standards, test coverage, duplications, API documentation, complexity, and architecture in addition to issues.&lt;/li&gt;
&lt;li&gt;  It provides you with a current snapshot of the quality of your Android app code as well as trends in trailing (what has previously gone wrong) and leading (what is most likely to go wrong in the future) quality indicators.&lt;/li&gt;
&lt;li&gt;  It offers measurements to guide you in making the best choice for your Android app.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  SonarScanner
&lt;/h3&gt;

&lt;p&gt;The SonarScanner CLI is a scanner that can be used every time if there is no specific scanner for your build system. Additionally, the SonarScanner CLI can be run within a Docker container to test the Android app source code.&lt;/p&gt;

&lt;p&gt;It is a simple command line tool that scans a provided folder (the Android app folder) with SonarQube-specific roles. After a successful analysis, it uploads all data to a SonarQube server where the results can be viewed.&lt;/p&gt;
&lt;h2&gt;
  
  
  Prepare Dockerfiles
&lt;/h2&gt;

&lt;p&gt;Now, after explaining the prerequisites and tools they will be combined in Docker Compose files to be then used in a single bash script that will result in the &lt;em&gt;&lt;strong&gt;software-quality-chain.&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  SonarQube/mobsfscan Docker Compose File
&lt;/h3&gt;

&lt;p&gt;As described earlier SonarQube and mobsfscan will be used within the Android &lt;strong&gt;software-quality-chain&lt;/strong&gt;. To be able to use them they will be deployed with Docker Compose as a service onto your operating system.&lt;/p&gt;

&lt;p&gt;The following Compose file contains the SonarQube server, a PostgreSQL database for SonarQube, and the mobsfscan container to scan your Android app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3"&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;sonarqube&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sonarqube:9.6-community&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;sonarqube_db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;SONAR_JDBC_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jdbc:postgresql://sonarqube_db:5432/sonar&lt;/span&gt;
      &lt;span class="na"&gt;SONAR_JDBC_USERNAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sonar&lt;/span&gt;
      &lt;span class="na"&gt;SONAR_JDBC_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sonar&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;wget --no-verbose --tries=1 --spider http://localhost:9000/api/system/status || exit &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sonarqube_data:/opt/sonarqube/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sonarqube_extensions:/opt/sonarqube/extensions&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sonarqube_logs:/opt/sonarqube/logs&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9000:9000"&lt;/span&gt;
  &lt;span class="na"&gt;sonarqube_db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:12&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sonar&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sonar&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready"&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgresql:/var/lib/postgresql&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgresql_data:/var/lib/postgresql/data&lt;/span&gt;
  &lt;span class="na"&gt;mobsfscan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opensecurity/mobsfscan&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;sonarqube&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./../app/src:/src&lt;/span&gt;
    &lt;span class="na"&gt;entrypoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mobsfscan&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--sonarqube&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;-o&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/src/result.json&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/src&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;sonarqube_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;sonarqube_extensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;sonarqube_logs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgresql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgresql_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within this file three services are defined: SonarQube, PostgreSQL, and mobsfscan. While SonarQube and PostgreSQL will just use a standardized Docker service that gets started, mobsfscan will have the project root bound as a volume and an overwritten entry point that executes instantly after the SonarQube container is started correctly and marked healthy (see healthcheck).&lt;/p&gt;

&lt;h3&gt;
  
  
  SonarScanner Compose File
&lt;/h3&gt;

&lt;p&gt;Luckily, SonarScanner utility is also available as Docker Image and can be used and configured in a Compose file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.7"&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;sonarscanner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sonarsource/sonar-scanner-cli&lt;/span&gt;
    &lt;span class="na"&gt;network_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host"&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.sq.env&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;-Dsonar.projectKey=YourAppName&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;-Dsonar.exclusions=**/*.java&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;-Dsonar.externalIssuesReportPaths=./output/result.json&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;-Dsonar.kotlin.detekt.reportPaths=./app/build/reports/detekt.xml&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./../:/usr/src"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see in this file we define a projectKey (YourAppName), set some exclusions (comma-separated list), and also add additional reports to the execution of SonarScanner (relative path from &lt;code&gt;/usr/src&lt;/code&gt;). Also, we add the Android project root as volume and map it to &lt;code&gt;/usr/src&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Additionally, this Compose file needs an environment file that will contain the SonarQube URL and a User Token which will be used to upload the analysis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SONAR_HOST_URL=http://localhost:9000
SONAR_LOGIN=squ_3d9c193d3699d18db4f539725090d67caef0b964
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Combining the Tools
&lt;/h2&gt;

&lt;p&gt;Now, all Dockerfiles will be combined within the bash script called &lt;strong&gt;&lt;em&gt;software-quality-chain&lt;/em&gt;&lt;/strong&gt; which consists of these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Increasing the heap count of the Docker environment&lt;/li&gt;
&lt;li&gt; Install, configure, and start SonarQube and mobsfscan&lt;/li&gt;
&lt;li&gt; Waiting until mobsfscan finish the analysis&lt;/li&gt;
&lt;li&gt; Running complete Gradle checks&lt;/li&gt;
&lt;li&gt; Importing a clean PostgreSQL database.&lt;/li&gt;
&lt;li&gt; Executing SonarScanner and combining results before uploading to SonarQube&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This bash script contains the final solution which will execute all steps:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Removing old output"&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ../output/detekt.xml
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ../output/result.json

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Increasing heap count for SonarQube"&lt;/span&gt;
sysctl &lt;span class="nt"&gt;-w&lt;/span&gt; vm.max_map_count&lt;span class="o"&gt;=&lt;/span&gt;262144

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Starting SonarQube, mobsfscan"&lt;/span&gt;
&lt;span class="nv"&gt;DOCKER_BUILDKIT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 docker-compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.sq.yml up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"SonarQube is started, running mobsfscan"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Waiting until scan is succesfully finished"&lt;/span&gt;
&lt;span class="k"&gt;until&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ../app/src/result.json &lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;do
     &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;5
&lt;span class="k"&gt;done

&lt;/span&gt;&lt;span class="nb"&gt;mv&lt;/span&gt; ../app/src/result.json ../output/result.json

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"finished mobsfscan"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"running gradle detekt"&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ..
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/baseline = file("..\/detekt\/baseline.xml")/\/\/baseline = file("..\/detekt\/baseline.xml")/g'&lt;/span&gt; app/build.gradle
./gradlew detekt
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/\/\/baseline = file("..\/detekt\/baseline.xml")/baseline = file("..\/detekt\/baseline.xml")/g'&lt;/span&gt; app/build.gradle
&lt;span class="nb"&gt;cd &lt;/span&gt;docker
&lt;span class="nb"&gt;cp&lt;/span&gt; ../app/build/reports/detekt.xml ../output/detekt.xml
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"finished gradle detekt"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"preparing files to import into SonarQube"&lt;/span&gt;
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/\/src\/main\//app\/src\/main\//g'&lt;/span&gt; ../output/result.json
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/"filePath": null/"filePath": "app\/src\/main\/AndroidManifest.xml"/g'&lt;/span&gt; ../output/result.json

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Restoring SonarQube database with default user"&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; docker-sonarqube_db-1 //bin//bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"PGPASSWORD=sonar psql --username sonar sonar"&lt;/span&gt; &amp;lt; dump.sql

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Starting SonarScanner"&lt;/span&gt;
&lt;span class="nv"&gt;DOCKER_BUILDKIT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1  docker-compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.scan.yml up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"SonarScanner started, will scan code now and upload data"&lt;/span&gt;
docker logs &lt;span class="nt"&gt;-f&lt;/span&gt; docker-sonarscanner-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Increasing the heap count of the Docker environment:
&lt;/h3&gt;

&lt;p&gt;To execute the SonarQube server without any problems you have to increase the max_map_count of the operating system running the Docker container because the default value is not sufficient.&lt;/p&gt;

&lt;p&gt;You can read about it in &lt;a href="https://docs.sonarqube.org/latest/requirements/requirements/?ref=paulsblog.dev"&gt;the SonarQube documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install, configure, and start SonarQube and mobsfscan:
&lt;/h3&gt;

&lt;p&gt;As the Docker Compose files already contain every important setting the only command that has to be executed here is running the Compose file in the background&lt;/p&gt;

&lt;h3&gt;
  
  
  Waiting until mobsfscan finish the analysis:
&lt;/h3&gt;

&lt;p&gt;Unfortunately, the Docker Compose service of mobsfscan is only started by running the Compose file and the analysis is executed afterward. Because of this, the script has to block until the mobsfscan analysis finishes and successfully creates a result file. This "blocking" is done in lines 16 - 21 where the script waits until the result.json is created and then moves it to the output folder.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running complete Gradle Detekt checks:
&lt;/h3&gt;

&lt;p&gt;The next step is executing a complete gradle detekt task. It's called complete because sometimes developers use a baseline.xml file in their detekt configuration to mark legacy code as "ignored". Within this step, the baseline.xml file will be removed by commenting it out with sed and then running the gradle detekt task. Afterward, the detekt report will be copied to the output folder, and the usage of &lt;code&gt;baseline.xml&lt;/code&gt; file will be restored.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preparing files to import into SonarQube:
&lt;/h3&gt;

&lt;p&gt;There are two small bugs if combining mobsfscan utility and SonarQube while creating and importing the result.json.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; mobsfscan also finds issues in the project that is not suited to specific source files. Unfortunately, these issues cannot be imported correctly into SonarQube because SonarQube always needs a file to add an issue. Luckily, it is easy to identify these issues because they all have &lt;code&gt;filePath=null&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; mobsfscan lists all file paths of files from the app folder while SonarQube lists all files from the project folder.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both issues will be fixed in line 35 and 36 where &lt;code&gt;sed&lt;/code&gt; is used to replace the wrong entries with the correct one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Line 35:&lt;/strong&gt; &lt;code&gt;src/main&lt;/code&gt; -&amp;gt; &lt;code&gt;app/src/main&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Line 36:&lt;/strong&gt; &lt;code&gt;filePath=null&lt;/code&gt; -&amp;gt; &lt;code&gt;filePath="app/src/main/AndroidManifest.xml"&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Importing a clean PostgreSQL database:
&lt;/h3&gt;

&lt;p&gt;Before starting the SonarScanner to analyze the source code and upload the results to the SonarQube instance a clean PostgreSQL database backup will be imported into the SonarQube instance (Line 39). This has to be done because the freshly installed SonarQube instance does not contain a User Token that can be used to upload data. The provided PostgreSQL backup contains a User (admin:admin12345) and a User Token that already is set within the SonarScanner environment variables.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Without this step, you would have to switch to your SonarQube instance and log in with the default user (admin:admin). Then you have to switch to your account configuration and create a new User Token that can be used for analysis.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Executing SonarScanner and combining results before uploading to SonarQube:
&lt;/h3&gt;

&lt;p&gt;The last step will be executing the Compose file for the SonarScanner (Line 42) and opening the docker logs for this instance to be notified if the analysis is complete.&lt;/p&gt;

&lt;p&gt;Afterward, you will get an URL within the terminal that you can open in the browser, log in with &lt;em&gt;&lt;strong&gt;admin:admin12345,&lt;/strong&gt;&lt;/em&gt; and see the results of the executed analysis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the Chain
&lt;/h2&gt;

&lt;p&gt;Now, that everything is set upped and explained you can start software-quality-chain. If you followed the tutorial and created the files you still need &lt;a href="https://github.com/paulknulst/android-software-quality-chain/blob/master/docker/dump.sql?ref=paulsblog.dev"&gt;this PostgreSQL database dump&lt;/a&gt; that you can save as &lt;code&gt;dump.sql&lt;/code&gt; within the docker folder. If you did not manually create every file you can &lt;a href="https://github.com/paulknulst/android-software-quality-chain?ref=paulsblog.dev"&gt;clone the complete GitHub repository&lt;/a&gt; into your Android project root.&lt;/p&gt;

&lt;p&gt;Then you can run it by executing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sh software-quality-chain.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Grab a coffee and wait until the chain finishes. There will be an URL within the CLI that you can copy to open the SonarQube instance. Log in with &lt;strong&gt;admin:admin12345&lt;/strong&gt; and start checking your improvements, security issues, and other stuff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Possible Errors
&lt;/h2&gt;

&lt;p&gt;If you are using this software-quality-chain you maybe encounter some errors that can be fixed easily!&lt;/p&gt;

&lt;h3&gt;
  
  
  SonarQube does not start
&lt;/h3&gt;

&lt;p&gt;As told before, to run SonarQube you have to increase the max_map_count. Normally, this will be done by the command in Line 8. If you are working with Windows or do not have permission you have to update the command:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Windows:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wsl sysctl -w vm.max_map_count=262144
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Not able because of permissions:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo sysctl -w vm.max_map_count=262144
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cannot run docker-compose (Windows)
&lt;/h3&gt;

&lt;p&gt;If you do not have Docker Desktop (or Rancher Desktop) it could happen that docker-compose cannot be executed from your terminal.&lt;/p&gt;

&lt;p&gt;Sometimes it happens because you have to add wsl in front of every docker-compose command (c.f. Line 11):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DOCKER_BUILDKIT=1 wsl docker-compose -f docker-compose.sq.yml up -d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cannot import PostgreSQL backup (Windows)
&lt;/h3&gt;

&lt;p&gt;If you try to run the software-quality-chain within the Powershell or Git-bash while working with windows you normally will encounter an error while the script tries to restore the PostgreSQL database.&lt;/p&gt;

&lt;p&gt;This happens because of a problem with the file encoding of the PostgreSQL dump that is used. To fix this problem connect to your WSL and restart the &lt;strong&gt;software-quality-chain&lt;/strong&gt;. Alternatively, you can log into the running SonarQube instance with default user &lt;strong&gt;admin:admin&lt;/strong&gt;, switch to accounts settings, and create a new user token. Then you can update the &lt;code&gt;sq.env&lt;/code&gt; file to use this new user token. If you do this you have to remove the part from the &lt;strong&gt;software-quality-chain&lt;/strong&gt; where the PostgreSQL backup will be inserted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Software-Quality-Chain Improvements/Changes
&lt;/h2&gt;

&lt;p&gt;As this chain is mainly suited for developers that want to run a quick full test for their android apps on their own computer, this chain could also be changed in some ways.&lt;/p&gt;

&lt;p&gt;An important feature of the described chain is that it will run on ANY device running Docker because it automatically inserts a database while executing. This means that every time you run this chain you will have a freshly installed SonarQube that only contains results from the executed analysis.&lt;/p&gt;

&lt;p&gt;But if you want to have a curve where you can see improvements while developing you have to persist the data from every consecutive analysis. Also, you can work with several members on the same SonarQube instance and use this chain within your CI/CD pipeline to automatically check after a Pull Request is merged into your release branch.&lt;/p&gt;

&lt;p&gt;The following chapters cover changes that have to be made to use the chain in any of these ways.&lt;/p&gt;

&lt;h3&gt;
  
  
  Persisting Analysis Data
&lt;/h3&gt;

&lt;p&gt;To persist data between consecutive analyses you simply have to adjust the &lt;strong&gt;software-quality-chain&lt;/strong&gt; after the first run.&lt;/p&gt;

&lt;p&gt;After your first successful run of the chain switch to lines 38 and 39 where the database import takes place.&lt;/p&gt;

&lt;p&gt;You still need the first run because it will populate the database to have a working user token and the user to log in &lt;em&gt;&lt;strong&gt;admin&lt;/strong&gt;&lt;/em&gt;:&lt;em&gt;&lt;strong&gt;admin12345&lt;/strong&gt;&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use a different SonarQube instance
&lt;/h3&gt;

&lt;p&gt;If you are using a different SonarQube instance within the &lt;strong&gt;software-quality-chain&lt;/strong&gt; you have to adjust the two files:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;docker-compose.sq.yml&lt;/strong&gt;: Remove the SonarQube and PostgreSQL container and only keep the mobsfscan. The Compose file will look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3"&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mobsfscan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opensecurity/mobsfscan&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./../app/src:/src&lt;/span&gt;
    &lt;span class="na"&gt;entrypoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mobsfscan&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--sonarqube&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;-o&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/src/result.json&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/src&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;sq.env:&lt;/strong&gt; Replace the URL with your SonarQube instance URL and update the Token to your user token&lt;/p&gt;

&lt;p&gt;Also, remove the PostgreSQL database backup (Line 38/39).&lt;/p&gt;

&lt;h3&gt;
  
  
  Use it in CI/CD chain
&lt;/h3&gt;

&lt;p&gt;To use this script in a CI/CD chain you have to combine previous sections. Remove the SonarQube instance by editing the Compose file, changing the URL/Token of &lt;code&gt;sq.env&lt;/code&gt;, and removing the PostgreSQL backup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Tools
&lt;/h2&gt;

&lt;p&gt;To further improve the quality of Android apps it is possible to enhance your Android project with two additional extra tools/commands that work well in combination with the &lt;strong&gt;software-quality-chain&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  SonarLint Plugin for Android
&lt;/h3&gt;

&lt;p&gt;The SonarLint plugin can be &lt;a href="https://plugins.jetbrains.com/plugin/7973-sonarlint?ref=paulsblog.dev"&gt;downloaded and installed manually from the JetBrains marketplace&lt;/a&gt; or you can easily install it from within Android Studio by opening Settings-&amp;gt;Plugins and installing it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pre-Push Hook For Executing Gradle Detekt
&lt;/h3&gt;

&lt;p&gt;To guarantee that all pushed files did not contain any error that will be found with Gradle you can install a pre-push hook into your .git folder. To do this open .git/hooks, create a new file called pre-push, make it executable (&lt;code&gt;chmod +x&lt;/code&gt;), and paste the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Starting detekt check"&lt;/span&gt;
./gradlew detekt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Closing Notes
&lt;/h2&gt;

&lt;p&gt;I hope you enjoyed reading this article and will now use my &lt;strong&gt;software-quality-chain&lt;/strong&gt; to significantly &lt;strong&gt;improve your code quality&lt;/strong&gt; and &lt;strong&gt;make your app more secure&lt;/strong&gt;. Keep in mind that software that has high quality can be maintained and enhanced more easily!&lt;/p&gt;

&lt;p&gt;All files that were explained in this tutorial can be found &lt;a href="https://github.com/paulknulst/android-software-quality-chain?ref=paulsblog.dev"&gt;in this GitHub repository&lt;/a&gt;. Download the whole folder, put it into your Android project root, and execute the &lt;strong&gt;software-quality-chain&lt;/strong&gt; with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sh software-quality-chain.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep in mind that on Windows you have to be connected to the WSL!&lt;/p&gt;

&lt;p&gt;This is the end of this tutorial. Hopefully, you are now able to use my &lt;strong&gt;Android software-quality-chain&lt;/strong&gt;. If you still have questions about anything that is not fully described you can just ask in the comment section. Also, if you enjoyed reading this article consider commenting with your valuable thoughts! I would love to hear your feedback about my developed chain.&lt;/p&gt;

&lt;p&gt;This article was originally published on my blog at &lt;a href="https://www.paulsblog.dev/optimize-android-app-development-with-docker-sonarqube-detekt-and-mobsf/"&gt;https://www.paulsblog.dev/optimize-android-app-development-with-docker-sonarqube-detekt-and-mobsf/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to connect with me on &lt;a href="https://www.paulsblog.dev"&gt;my blog&lt;/a&gt;, &lt;a href="https://medium.knulst.de/"&gt;Medium&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/paulknulst/"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://twitter.com/paulknulst"&gt;Twitter&lt;/a&gt;, and &lt;a href="https://github.com/paulknulst"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;🙌 Support this content&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you like this content, please consider &lt;a href="https://www.paulsblog.dev/support/"&gt;supporting me&lt;/a&gt;. You can share it on social media or &lt;a href="https://buymeacoffee.com/paulknulst"&gt;buy me a coffee&lt;/a&gt;! Any support helps!&lt;/p&gt;

&lt;p&gt;Furthermore, you can &lt;a href="https://www.paulsblog.dev/#/portal/signup/"&gt;sign up for my newsletter&lt;/a&gt; to show your contribution to my content. See the &lt;a href="https://www.paulsblog.dev/contribute/"&gt;contribute page&lt;/a&gt; for all (free or paid) ways to say thank you!&lt;/p&gt;

&lt;p&gt;Thanks! 🥰&lt;/p&gt;




&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@jamomca?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText"&gt;Jamison McAndie&lt;/a&gt; / &lt;a href="https://unsplash.com/?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>docker</category>
      <category>android</category>
      <category>devops</category>
    </item>
    <item>
      <title>Use NestJS, MongoDB and Docker to Create an URL Shortener</title>
      <dc:creator>Paul Knulst</dc:creator>
      <pubDate>Wed, 19 Apr 2023 05:52:28 +0000</pubDate>
      <link>https://dev.to/paulknulst/use-nestjs-mongodb-and-docker-to-create-an-url-shortener-226h</link>
      <guid>https://dev.to/paulknulst/use-nestjs-mongodb-and-docker-to-create-an-url-shortener-226h</guid>
      <description>&lt;p&gt;&lt;strong&gt;Implement a Simple URL Shortener With NestJS, Docker, MongoDB, And Deploy It to Production With SSL Enabled Using Traefik. Also, Docker Swarm Setup&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.paulsblog.dev/author/paulknulst/"&gt;Paul Knulst&lt;/a&gt;  in  &lt;a href="https://www.paulsblog.dev/tag/programming/"&gt;Programming&lt;/a&gt; • Sep 6, 2022 • 10 min read&lt;/p&gt;




&lt;p&gt;This tutorial will cover all work to build a simple URL shortener API with NestJS and MongoDB. Additionally, I will show how to deploy it to a (Docker and Docker Swarm) production environment using Docker.&lt;/p&gt;

&lt;p&gt;The source code is published on GitHub and can be used freely:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/paulknulst/paulsshortener?ref=dev.to"&gt;https://github.com/paulknulst/paulsshortener&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Create the NestJS Project
&lt;/h2&gt;

&lt;p&gt;First, create a NestJS project which will work as a baseline for the URL shortener. To do this, you need to install the &lt;a href="https://github.com/nestjs/nest-cli?ref=paulsblog.dev"&gt;Nest-CLI&lt;/a&gt; on your system which can be done by:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @nestjs/cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then switch to your projects folder and use the Nest-CLI to create a new NestJS project by executing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nest new paulsshortener
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During this process, you will be asked which package manager you want to use. Within this tutorial &lt;code&gt;npm&lt;/code&gt; will be used.&lt;/p&gt;




&lt;p&gt;Now, you  can start the NestJS project in "&lt;em&gt;watch-mode&lt;/em&gt;" to instantly see all changes you will make during this tutorial by executing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run start:dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To test if everything is started correctly hit &lt;a href="http://localhost:3000/"&gt;http://localhost:3000&lt;/a&gt; which will just show "Hello World" within the browser.&lt;/p&gt;

&lt;p&gt;Within the project open the AppService (&lt;code&gt;/src/app.service.ts&lt;/code&gt;) and change &lt;code&gt;return 'Hello World!';&lt;/code&gt; to &lt;code&gt;return 'This will be your URL shortener';&lt;/code&gt;. After reloading &lt;a href="http://localhost:3000"&gt;http://localhost:3000&lt;/a&gt; the updated response will be seen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set Up MongoDB database
&lt;/h2&gt;

&lt;p&gt;For simplicity, MongoDB will be used during this tutorial. To avoid trouble setting up the correct version, replacing an old installation, and so on you should use Docker to deploy it. Save this minimal Compose file into your project root and name it &lt;code&gt;docker-compose.local.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.6'&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mongo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mongo&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MONGO_INITDB_ROOT_USERNAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
      &lt;span class="na"&gt;MONGO_INITDB_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;supersafe&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;If you are not familiar with Docker Compose and do not want to use it to set up a MongoDB you can have a look at &lt;a href="https://www.mongodb.com/docs/manual/installation/?ref=paulsblog.dev"&gt;the official MongoDB documentation&lt;/a&gt; to learn how you can install it on your machine. Keep in mind that Docker is also used to deploy this URL shortener later within this tutorial!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now, to deploy a MongoDB, switch to your project root within a terminal and execute:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Afterward, you have successfully set up MongoDB and your machine and can start using it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connect Your NestJS project to MongoDB
&lt;/h2&gt;

&lt;p&gt;To connect the project to MongoDB you should use Mongoose which is the most popular MongoDB object modeling tool. Start by installing the required dependencies into your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i @nestjs/mongoose mongoose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you have installed the dependency you can import the Mongoose module into the project by editing the AppModule (&lt;code&gt;/src/app.module.ts&lt;/code&gt;) that it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&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;Module&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="s1"&gt;@nestjs/common&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;AppController&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="s1"&gt;./app.controller&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;AppService&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="s1"&gt;./app.service&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;MongooseModule&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="s1"&gt;@nestjs/mongoose&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="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;MongooseModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mongodb://localhost:12345/paulsshortener&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
  &lt;span class="na"&gt;controllers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;AppController&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;AppService&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;class&lt;/span&gt; &lt;span class="nx"&gt;AppModule&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The connection will now be automatically established and you can create the schema that will be used to work with the database.&lt;/p&gt;

&lt;p&gt;Now use the Nest-CLI to create a new module within your NestJS project which will handle everything related to URLs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nest g res url
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The routine will ask two questions that you should answer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  What transport layer do you use? &lt;strong&gt;-&amp;gt; REST API&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  Would you like to generate CRUD entry points? &lt;strong&gt;-&amp;gt; No&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Switch to the newly generated &lt;code&gt;url&lt;/code&gt; folder, create a new folder schema, and add a file &lt;code&gt;url.schema.ts&lt;/code&gt;. Then create a class (&lt;code&gt;Url&lt;/code&gt;) add two properties (&lt;code&gt;url&lt;/code&gt;, &lt;code&gt;shortenedUrl&lt;/code&gt;) to the file. Also, add exports for Document and Schema. Your file should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&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;Prop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SchemaFactory&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="s1"&gt;@nestjs/mongoose&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;Document&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="s1"&gt;mongoose&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="nx"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;UrlDocument&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Url&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Schema&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;class&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="nd"&gt;Prop&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;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Prop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;shortenedUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&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;UrlSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SchemaFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createForClass&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open up &lt;code&gt;url.module.ts&lt;/code&gt;, add two new imports, and modify the &lt;code&gt;@Module&lt;/code&gt; annotation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&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;Module&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="s1"&gt;@nestjs/common&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;UrlService&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="s1"&gt;./url.service&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;UrlController&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="s1"&gt;./url.controller&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;Url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;UrlSchema&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;./schemas/url.schema&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;MongooseModule&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;@nestjs/mongoose&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="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;MongooseModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forFeature&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt;&lt;span class="na"&gt;name&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;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UrlSchema&lt;/span&gt;&lt;span class="p"&gt;}])],&lt;/span&gt;
    &lt;span class="na"&gt;controllers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;UrlController&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;UrlService&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;class&lt;/span&gt; &lt;span class="nx"&gt;UrlModule&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;
  
  
  Implement the URL Link Shortner Function
&lt;/h2&gt;

&lt;p&gt;To shrink an URL you will be going to use the CRC32 hash algorithm which has to be added to the project to use it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;crc-32 &lt;span class="nt"&gt;--save&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the UrlService (&lt;code&gt;/src/url/url.service.ts&lt;/code&gt;) and replace the content of the file with the following snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&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;Injectable&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="s1"&gt;@nestjs/common&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;InjectModel&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;@nestjs/mongoose&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;Url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;UrlDocument&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;./schemas/url.schema&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;Model&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;mongoose&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;CRC&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;crc-32&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="nd"&gt;Injectable&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;class&lt;/span&gt; &lt;span class="nx"&gt;UrlService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;InjectModel&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;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kr"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;urlModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Model&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UrlDocument&lt;/span&gt;&lt;span class="o"&gt;&amp;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;span class="kr"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;shrink&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;string&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;CRC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;str&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;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;create&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;string&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;createdUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urlModel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;url&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="na"&gt;shortenedUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shrink&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;createdUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;save&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;createdUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shortenedUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shortenedUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;//-3c666ac&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="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urlModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;shortenedUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;shortenedUrl&lt;/span&gt;&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;exec&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;url&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="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;This snippet contains three important functions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;shrink:&lt;/strong&gt; Converts the given URL into an 8-character string by using the CRC32 algorithm. Then returns only the 8-character string.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;create:&lt;/strong&gt; Use the shrink function to create an 8-character string and saves a new UrlSchema document into the MongoDB&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;find:&lt;/strong&gt; Retrieves the saved URL from the MongoDB.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Add Routes to NestJS API
&lt;/h2&gt;

&lt;p&gt;To use the URL shortener in a Client we need to create three REST endpoints/functions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;GET /shrink:&lt;/strong&gt; Create a new shortened URL using the HTTP Get method. The path parameter contains the unshortened URL. This can be done within any browser.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;POST /shrink:&lt;/strong&gt; Create a new shortened URL using the HTTP Post method. The body contains the unshortened URL. You need an API or Postman to use this.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;GET  /s:&lt;/strong&gt; Takes the 8-character string as a path parameter and returns the unshortened URL. In the end, it will automatically forward to the unshortened URL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As you are working with NestJS you can easily implement these endpoints by adding three new functions to the UrlController (&lt;code&gt;url.controller.ts&lt;/code&gt;) within the URL module and annotate it with &lt;code&gt;@Get&lt;/code&gt; and &lt;code&gt;@Post&lt;/code&gt;. Open the UrlController and replace the content with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&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;Body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Post&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="s1"&gt;@nestjs/common&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;UrlService&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="s1"&gt;./url.service&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="nd"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;(&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;class&lt;/span&gt; &lt;span class="nx"&gt;UrlController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;urlService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UrlService&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;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/shrink/:url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;getShrink&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;url&lt;/span&gt;&lt;span class="dl"&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;string&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urlService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/shrink&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;postShrink&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urlService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/s/:shortenedUrl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;unshrink&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shortenedUrl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;shortenedUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urlService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shortenedUrl&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;p&gt;Within this snippet, two new GET resources (&lt;strong&gt;/shrink/&lt;/strong&gt; and &lt;strong&gt;/s/&lt;/strong&gt;) and one POST resource that calls the previously created functions from the UrlService are created. Also, the annotation above the class is changed from &lt;code&gt;@Controller('url')&lt;/code&gt;  to &lt;code&gt;@Controller('')&lt;/code&gt; to further shrink the resulting URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the URL shortener
&lt;/h2&gt;

&lt;p&gt;To test the functionality you can simply use your browser because we have implemented both resources as GET calls.&lt;/p&gt;

&lt;p&gt;Keep in mind that as you use GET for providing an URL as a path parameter &lt;strong&gt;you have to encode the URL&lt;/strong&gt;! This means that an URL like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://www.paulsblog.dev/manage-time-more-efficiently-with-the-pomodoro-technique/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;will become this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https%3A%2F%2Fwww.paulsblog.dev%2Fmanage-time-more-efficiently-with-the-pomodoro-technique%2F
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this information, you can create a shortened URL by opening the following URL in our browser:&lt;/p&gt;

&lt;p&gt;&lt;a href="http://localhost:3000/shrink/https%3A%2F%2Fwww.paulsblog.dev%2Fmanage-time-more-efficiently-with-the-pomodoro-technique%2F"&gt;http://localhost:3000/shrink/https%3A%2F%2Fwww.paulsblog.dev%2Fmanage-time-more-efficiently-with-the-pomodoro-technique%2F&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With this GET call your API will return the 8-character string: &lt;strong&gt;-5f1a8349&lt;/strong&gt; (It should be the same within your project)&lt;/p&gt;

&lt;p&gt;Append this string to your GET /s/ call to receive the unshortened version of the URL by opening: &lt;a href="http://localhost:3000/s/-5f1a8349"&gt;http://localhost:3000/s/-5f1a8349&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The result in the browser will be the unshortened version of the previously provided URL.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Implement Forwarding to Unshortened URL
&lt;/h2&gt;

&lt;p&gt;Now, that you have developed an API that can shorten URLs with help of the CRC-32 algorithm you should enable the functionality to forward the request and automatically open the URL it finds within the database.&lt;/p&gt;

&lt;p&gt;With NestJS this is easy because you only have to adjust the unshrink function within the UrlController (&lt;code&gt;src/url/url.controller.ts&lt;/code&gt;). Change the previously created function to this implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/s/:shortenedUrl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;unshrink&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Res&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shortenedUrl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;shortenedUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urlService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shortenedUrl&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;redirect&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deploy The URL Shortener With Docker
&lt;/h2&gt;

&lt;p&gt;Let's assume you want to deploy the URL shortener in a Docker environment and have to develop a Compose file that can be used to do this.&lt;/p&gt;

&lt;p&gt;The first step to do will be to create a new Compose file (&lt;code&gt;docker-compose.prod.yml&lt;/code&gt;) and copy the content from the previously created MongoDB file into it. Then add a new service called backend which will be used to install, compile and run the NestJS project within the Docker environment.&lt;/p&gt;

&lt;p&gt;As you have developed a custom piece of software there will not be a suitable image on DockerHub and you have to create one from scratch. Because the project is based on NestJS, which is working in a NodeJS environment, you can create a new Dockerfile and use the latest version of &lt;code&gt;node&lt;/code&gt; as a base image. Then simply copy the source code, install, build and run the project. The following Dockerfile will be sufficient and should be created in the project root:&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; node:latest&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /usr/src/app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&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;npm run build

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; [ "node", "dist/main.js" ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This Dockerfile can now be used as the image for the backend service in the Compose file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.6'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mongo&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MONGODB_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paul&lt;/span&gt;
      &lt;span class="na"&gt;MONGODB_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paulsshortener&lt;/span&gt;
      &lt;span class="na"&gt;MONGODB_PASS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paulspw&lt;/span&gt;
  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paulsshortener&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;3001:3000&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unfortunately, running this Compose file will not work because the URL for the links within our project are hard-coded and will not automatically adjust. Also, the DB hostname and port within the AppModule (&lt;code&gt;app.module.ts&lt;/code&gt;) are static.&lt;/p&gt;

&lt;p&gt;To fix these problems, you have to add and change something within the AppModule (&lt;code&gt;app.module.ts&lt;/code&gt;) and the UrlService (&lt;code&gt;url.service.ts&lt;/code&gt;). To be specific, add three new variables that hold the database port, the database URL, and the base URL of the resulting service.&lt;/p&gt;

&lt;p&gt;Switch to the AppModule (&lt;code&gt;app.module.ts&lt;/code&gt;) and add these two variables above the class definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DB_HOST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB_HOST&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;localhost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DB_PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB_PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;12345&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Additionally, change the Mongoose part within the imports from the module to correctly use these variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;MongooseModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mongodb://&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;DB_HOST&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;DB_PORT&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/paulsshortener&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open the UrlService (&lt;code&gt;url.service.ts&lt;/code&gt;) and add the &lt;code&gt;baseurl&lt;/code&gt; variable above the class definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;basepath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:3000/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lastly, change the create function to return the complete shortened URL using the newly introduced base variable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;create&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;string&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;createdUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urlModel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;url&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="na"&gt;shortenedUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shrink&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;createdUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;save&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;basepath&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;s/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;createdUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shortenedUrl&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;p&gt;Now, that you applied these changes you should adjust your Compose file by adding the available environment variables and adjusting them to your needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.6'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mongo&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MONGODB_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paul&lt;/span&gt;
      &lt;span class="na"&gt;MONGODB_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paulsshortener&lt;/span&gt;
      &lt;span class="na"&gt;MONGODB_PASS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paulspw&lt;/span&gt;
  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paulsshortener&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;BASE_URL=https://locahost:3001&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_HOST=db&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_PORT=27017&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;3001:3000&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, deploy it on your localhost:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Your URL shortener is now correctly installed and can be used from your local environment.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy to Production Using Traefik
&lt;/h2&gt;

&lt;p&gt;Now that you have a running Docker Service that can be deployed anywhere you can use it to deploy it in a production environment using Traefik. To do this you will edit the Compose file, add all Traefik-related keywords (labels, networks), and adjust it to your needs.&lt;/p&gt;

&lt;p&gt;If you are not familiar with deploying Docker Services using Traefik I can recommend the following tutorials covering basic installation on a single server and a server cluster installation using Docker Swarm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;&lt;a href="https://levelup.gitconnected.com/how-to-setup-traefik-v2-with-automatic-lets-encrypt-certificate-resolver-83de0ed0f542?ref=dev.to"&gt;How to setup Traefik v2 with automatic Let’s Encrypt certificate resolver&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;a href="https://betterprogramming.pub/deploy-any-ssl-secured-website-with-docker-and-traefik-27fbeb1343d3?ref=dev.to"&gt;Deploy Any SSL Secured Website With Docker And Traefik&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;a href="https://levelup.gitconnected.com/docker-swarm-in-a-nutshell-ed2a9c42cd7c?ref=dev.to"&gt;Setup Docker Swarm (For Traefik)&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;a href="https://levelup.gitconnected.com/the-most-important-services-everyone-should-deploy-in-a-docker-swarm-8e120b5a66?ref=dev.to"&gt;Install Traefik On Docker Swarm&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To deploy your URL shortener using Traefik adjust the labels section. The following part will show and explain how it is done in a single server setup and additionally there will be a Docker Swarm configuration to download:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.6'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mongo&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MONGODB_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paul&lt;/span&gt;
      &lt;span class="na"&gt;MONGODB_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paulsshortener&lt;/span&gt;
      &lt;span class="na"&gt;MONGODB_PASS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paulspw&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db:/data/db&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paulsshortener&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;BASE_URL=https://at0m.de/&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_HOST=db&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_PORT=27017&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik-public&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.enable=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.docker.network=traefik-public&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.constraint-label=traefik-public&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.pauls-shortener-http.rule=Host(`at0m.de`) || Host(`www.at0m.de`)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.pauls-shortener-http.entrypoints=http&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.pauls-shortener-http.middlewares=https-redirect&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.pauls-shortener-https.rule=Host(`at0m.de`)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.pauls-shortener-https.entrypoints=https&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.pauls-shortener-https.tls=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.pauls-shortener-https.tls.certresolver=le&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.services.pauls-shortener.loadbalancer.server.port=3000&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.middlewares.redirect-pauls-shortener.redirectregex.regex=^https://www.at0m.de/(.*)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.middlewares.redirect-pauls-shortener.redirectregex.replacement=https://at0m.de/$${1}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.middlewares.redirect-pauls-shortener.redirectregex.permanent=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.blogs-knulst-https.middlewares=redirect-pauls-shortener&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;traefik-public&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before you can successfully deploy this service with Compose you have to adjust the Host: &lt;strong&gt;Use your own BASE_URL&lt;/strong&gt; and update the traefik configuration (For me, it is &lt;a href="https://at0m.de/?ref=dev.to"&gt;at0m.de&lt;/a&gt;). After changing both values deploy it with:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;






&lt;p&gt;Using a Docker Swarm you can use &lt;a href="https://raw.githubusercontent.com/paulknulst/paulsshortener/master/docker-compose.prod-swarm.yml"&gt;this Compose file&lt;/a&gt;. But, you have to adjust BASE_URL &lt;strong&gt;and&lt;/strong&gt; the placement constraints for the MongoDB service. Then &lt;strong&gt;build&lt;/strong&gt;, &lt;strong&gt;push&lt;/strong&gt; the image to our registry, and &lt;strong&gt;deploy&lt;/strong&gt; it onto your Docker Swarm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.prod-swarm.yml build
docker-compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.prod-swarm.yml push
docker stack deploy &lt;span class="nt"&gt;-c&lt;/span&gt; docker-compose.prod-swarm.yml paulsshortener
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Additional Adjustments for Live Version
&lt;/h2&gt;

&lt;p&gt;Because you deployed it publicly on your server you should add rate limiting by following this approach: &lt;a href="https://docs.nestjs.com/security/rate-limiting?ref=paulsblog.dev"&gt;https://docs.nestjs.com/security/rate-limiting&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  tl;dr:
&lt;/h3&gt;

&lt;p&gt;Install needed package within the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;--save&lt;/span&gt; @nestjs/throttler
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In AppModule (&lt;code&gt;app.module.ts&lt;/code&gt;) extend imports-array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;ThrottlerModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forRoot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&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;p&gt;Then, add ThrottlerGuard to the providers array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;provide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APP_GUARD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;useClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ThrottlerGuard&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;p&gt;Add the SkipThrottle annotation to the Get /s/ endpoint within the UrlController (&lt;code&gt;url.controller.ts&lt;/code&gt;) to ignore rate limiting this specific call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@SkipThrottle()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Finally&lt;/strong&gt;, redeploy your Docker service. &lt;strong&gt;Don't forget to rebuild before deploying!&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Notes
&lt;/h2&gt;

&lt;p&gt;I hope you enjoyed reading my tutorial and are now able to create, build, and deploy your URL shortener website within a Docker container.&lt;/p&gt;

&lt;p&gt;Keep in mind that this is a very basic example without any error handling, and no URL checking. However, this tutorial should be a starting point for developing your version.&lt;/p&gt;

&lt;p&gt;If you enjoyed reading this article consider commenting your valuable thoughts in the comments section. I would love to hear your feedback about this URL shortener. Furthermore, if you have any questions about implementing your own version, please jot them down below. I try to answer them if possible. Also, share this article with your friends and colleagues to show them how to use NestJs, MongoDB, and Docker to create their own URL shortener.&lt;/p&gt;

&lt;p&gt;This article was originally published on my blog at &lt;a href="https://www.paulsblog.dev/use-nestjs-mongodb-and-docker-to-create-an-url-shortener/"&gt;https://www.paulsblog.dev/use-nestjs-mongodb-and-docker-to-create-an-url-shortener/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to connect with me on &lt;a href="https://www.paulsblog.dev"&gt;my blog&lt;/a&gt;, &lt;a href="https://medium.knulst.de/"&gt;Medium&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/paulknulst/"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://twitter.com/paulknulst"&gt;Twitter&lt;/a&gt;, and &lt;a href="https://github.com/paulknulst"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;🙌 Support this content&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you like this content, please consider &lt;a href="https://www.paulsblog.dev/support/"&gt;supporting me&lt;/a&gt;. You can share it on social media or &lt;a href="https://buymeacoffee.com/paulknulst"&gt;buy me a coffee&lt;/a&gt;! Any support helps!&lt;/p&gt;

&lt;p&gt;Furthermore, you can &lt;a href="https://www.paulsblog.dev/#/portal/signup/free"&gt;sign up for my newsletter&lt;/a&gt; to show your contribution to my content. See the &lt;a href="https://www.paulsblog.dev/contribute/"&gt;contribute page&lt;/a&gt; for all (free or paid) ways to say thank you!&lt;/p&gt;

&lt;p&gt;Thanks! 🥰&lt;/p&gt;




&lt;p&gt;&lt;a href="https://www.freepik.com/free-vector/man-woman-with-chains-background_1086772.htm#query=chain&amp;amp;from_query=small%20chain&amp;amp;position=30&amp;amp;from_view=search"&gt;Image by Vectorarte&lt;/a&gt; on Freepik&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
      <category>node</category>
    </item>
    <item>
      <title>How To Host Your Website For Free</title>
      <dc:creator>Paul Knulst</dc:creator>
      <pubDate>Fri, 24 Mar 2023 12:50:36 +0000</pubDate>
      <link>https://dev.to/paulknulst/how-to-host-your-website-for-free-4944</link>
      <guid>https://dev.to/paulknulst/how-to-host-your-website-for-free-4944</guid>
      <description>&lt;p&gt;&lt;strong&gt;Learn How To Host Your Website On Multiple Different Sites For Free&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.paulsblog.dev/"&gt;Paul Knulst&lt;/a&gt;  in  &lt;a href="https://www.paulsblog.dev/tag/programming/"&gt;Programming&lt;/a&gt; • Jun 24, 2022 • 3 min read&lt;/p&gt;

&lt;p&gt;If you are a student or a professional, you typically work on some projects that you create in your free time for fun, for learning something new, or for your portfolio. Often, you also want that the projects could be shown to your fellow developers so that they can benefit from them. But only showing the code on GitHub is not sufficient because you want to show a running version that can be tested out by everyone before downloading your source code.&lt;/p&gt;

&lt;p&gt;Luckily, today exist multiple services that you can use to show your work to the public. Although there are some paid options like AWS, Azure, and GCP (if you are out of the free tier), I will introduce some services that you can use without spending any money on them.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://pages.github.com/?ref=paulsblog.dev"&gt;1. Github Pages⁣&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;GitHub Pages is a feature from GitHub that enables every user or company to host a public webpage directly from your GitHub repository. You can edit, push, and your changes will be live.&lt;/p&gt;

&lt;h4&gt;
  
  
  Pros of this solution:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;  Widely used&lt;/li&gt;
&lt;li&gt;  Free hosting&lt;/li&gt;
&lt;li&gt;  Fast and easy to setup&lt;/li&gt;
&lt;li&gt;  Great customer service&lt;/li&gt;
&lt;li&gt;  Great documentation&lt;/li&gt;
&lt;li&gt;  Security and reliable&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Cons of this solution:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;  GitHub pages are limited to static websites&lt;/li&gt;
&lt;li&gt;  Special measures should be done for SEO&lt;/li&gt;
&lt;li&gt;  No support for any problem. You have to rely on forums and documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.netlify.com/?ref=paulsblog.dev"&gt;2.  Netlify&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Netlify is an American cloud computing company that offers hosting and serverless backend services for web applications and static websites. Normally, you have to pay for their services but they provide &lt;strong&gt;a free plan for personal websites&lt;/strong&gt;. You can build, deploy, and host any static website or app with a drag and drop interface. Also, it can automatically deploy from GitHub or Bitbucket.&lt;/p&gt;

&lt;h4&gt;
  
  
  Pros of this solution:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;  Provides continuous integration and continuous deployment&lt;/li&gt;
&lt;li&gt;  Drag and drop functionality is intuitive&lt;/li&gt;
&lt;li&gt;  Customer support service is good&lt;/li&gt;
&lt;li&gt;  Easy deploy service and use&lt;/li&gt;
&lt;li&gt;  Support Custom domains&lt;/li&gt;
&lt;li&gt;  SSL support&lt;/li&gt;
&lt;li&gt;  Ability to use plugins to customize build workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Cons of this solution:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;  Dynamic content cannot be displayed&lt;/li&gt;
&lt;li&gt;  Supports only static websites&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://vercel.com/?ref=paulsblog.dev"&gt;3. Vercel&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;A cloud platform for serverless deployment. It enables developers to host websites and web services that deploy instantly, scale automatically, and require no supervision, all with minimal configuration. Vercel is a tool in the &lt;strong&gt;Static Web Hosting&lt;/strong&gt; category of a tech stack.&lt;/p&gt;

&lt;h4&gt;
  
  
  Pros of this solution:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;  Simple deployment&lt;/li&gt;
&lt;li&gt;  Easy, production-ready deployment with one command.&lt;/li&gt;
&lt;li&gt;  Simple scaling, including auto-scaling&lt;/li&gt;
&lt;li&gt;  SSR (Server-Side Rendering) support&lt;/li&gt;
&lt;li&gt;  Free tier&lt;/li&gt;
&lt;li&gt;  Free SSL&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Cons of this solution:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;  The platform is a bit confusing&lt;/li&gt;
&lt;li&gt;  Lack of guides and manuals&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.heroku.com/?ref=paulsblog.dev"&gt;4. Heroku&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Heroku is a cloud platform with a free tier that allows up to 1000 dyno hours per month and includes deployment from Git and Docker. Normally, it is used to build, deliver, monitor, and scale all kinds of apps in different languages/frameworks: Node.js, Ruby, Python, Java, and much more.&lt;/p&gt;

&lt;p&gt;Furthermore, with Heroku, you are allowed to use custom domains, container orchestration, and automatic OS patching.&lt;/p&gt;

&lt;h4&gt;
  
  
  Pros of this solution:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;  Deployments with Git and Docker&lt;/li&gt;
&lt;li&gt;  Easy to start&lt;/li&gt;
&lt;li&gt;  Custom domains&lt;/li&gt;
&lt;li&gt;  Free Tier&lt;/li&gt;
&lt;li&gt;  Container orchestration&lt;/li&gt;
&lt;li&gt;  Automatic OS patching&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Cons of this solution:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;  Cannot control exact configuration of applications&lt;/li&gt;
&lt;li&gt;  Could be too overwhelming&lt;/li&gt;
&lt;li&gt;  No SSL&lt;/li&gt;
&lt;li&gt;  Could stop working/has some downtime&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://firebase.google.com/?ref=paulsblog.dev"&gt;5. Firebase&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Firebase from Google is primarily an app development platform that could be used as a backend for apps and games. It provides several features like Authentication, Remote configuration, A/B testing, and analytics out of the box within the free tier (and more).&lt;/p&gt;

&lt;h3&gt;
  
  
  Pros of this solution:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  Authentication&lt;/li&gt;
&lt;li&gt;  Analytics&lt;/li&gt;
&lt;li&gt;  Test Lab&lt;/li&gt;
&lt;li&gt;  Free Tier&lt;/li&gt;
&lt;li&gt;  Free SSL&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cons of this solution:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  Limitations of real-time database&lt;/li&gt;
&lt;li&gt;  More complicated&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Closing Notes
&lt;/h3&gt;

&lt;p&gt;All of the mentioned services could be easily used to host your private project. Although some of them have more features than others you could pick the best service for a specific project. Furthermore, you can use every service if you have multiple projects that you want to show.&lt;/p&gt;

&lt;p&gt;Use them wisely to spread your work to the public crowd!&lt;/p&gt;

&lt;p&gt;If you enjoyed reading this article consider commenting your valuable thoughts in the comments section. I would love to hear your feedback about my tutorial. Furthermore, share this article with fellow developers to help them get hosting their own services for free!&lt;/p&gt;

&lt;p&gt;This article was originally published on my blog at &lt;a href="https://www.paulsblog.dev/how-to-host-your-website-for-free/"&gt;https://www.paulsblog.dev/how-to-host-your-website-for-free/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to connect with me on &lt;a href="https://www.paulsblog.dev"&gt;my blog&lt;/a&gt;, &lt;a href="https://medium.knulst.de/"&gt;Medium&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/paulknulst/"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://twitter.com/paulknulst"&gt;Twitter&lt;/a&gt;, and &lt;a href="https://github.com/paulknulst"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;🙌 Support this content&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you like this content, please consider &lt;a href="https://www.paulsblog.dev/support/"&gt;supporting me&lt;/a&gt;. You can share it on social media or &lt;a href="https://buymeacoffee.com/paulknulst"&gt;buy me a coffee&lt;/a&gt;! Any support helps!&lt;/p&gt;

&lt;p&gt;Furthermore, you can &lt;a href="https://www.paulsblog.dev/#/portal/signup/free"&gt;sign up for my newsletter&lt;/a&gt; to show your contribution to my content. See the &lt;a href="https://www.paulsblog.dev/contribute/"&gt;contribute page&lt;/a&gt; for all (free or paid) ways to say thank you!&lt;/p&gt;

&lt;p&gt;Thanks! 🥰&lt;/p&gt;




&lt;p&gt;&lt;a href="https://www.freepik.com/free-photo/young-engineer-is-showing-rock-gestures-with-hands-by-sitting-front-tablet-blue-background_26714573.htm#query=surprised%20developer&amp;amp;position=23&amp;amp;from_view=search&amp;amp;track=ais"&gt;Image by 8photo&lt;/a&gt; on Freepik&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How To Add Comments To Your Blog With Docker</title>
      <dc:creator>Paul Knulst</dc:creator>
      <pubDate>Mon, 27 Feb 2023 08:30:35 +0000</pubDate>
      <link>https://dev.to/paulknulst/how-to-add-comments-to-your-blog-with-docker-146e</link>
      <guid>https://dev.to/paulknulst/how-to-add-comments-to-your-blog-with-docker-146e</guid>
      <description>&lt;p&gt;&lt;strong&gt;Having Comments On Your Own Blog Is One Of The Most Engaging Features. Learn How To Integrate Isso Comments To Have Privacy-Focused Comment Feature In Your Blog.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.paulsblog.dev/author/paulknulst/" rel="noopener noreferrer"&gt;Paul Knulst&lt;/a&gt;  in  &lt;a href="https://www.paulsblog.dev/tag/docker/" rel="noopener noreferrer"&gt;Docker&lt;/a&gt; • Jul 1, 2022 • 7 min read&lt;/p&gt;




&lt;p&gt;Having comments on your blog is one of the most engaging features! Unfortunately, Ghost Blogging Service does not support any comments out of the box. Although they are many different services like Disqus or Discourse which are kind of "free" (they have inbuilt Ads in the "free" version. And Ads are  &lt;strong&gt;EVIL&lt;/strong&gt;) there also exist some really free services like &lt;a href="https://github.com/adtac/commento" rel="noopener noreferrer"&gt;Commento&lt;/a&gt;, &lt;a href="https://github.com/schn4ck/schnack" rel="noopener noreferrer"&gt;Schnack&lt;/a&gt;, &lt;a href="https://github.com/coralproject/talk" rel="noopener noreferrer"&gt;CoralProject Talk&lt;/a&gt;, and &lt;a href="https://github.com/posativ/isso/" rel="noopener noreferrer"&gt;Isso&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I tested all of the free services for my blog and come to the conclusion that Isso is the best service to use in a Ghost Blog. Within this article, I will describe why Isso is the best software, how it can be installed in a Docker environment, and how Isso comments can be integrated into any Blog (not only Ghost Blogging software).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Isso?
&lt;/h2&gt;

&lt;p&gt;Isso is a commenting server written in Python and JavaScript and aims to be a drop-in replacement for Disqus or Discourse.&lt;/p&gt;

&lt;p&gt;It has several features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  It is a very lightweight commenting system&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Works with Docker Compose&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  Uses SQLite because comments are not Big Data!&lt;/li&gt;
&lt;li&gt;  A very minimal commenting system with a simple moderation system&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Privacy-first commenting system&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  Supports Markdown&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;It's free&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  Similar to native WordPress comments&lt;/li&gt;
&lt;li&gt;  You can Import WordPress or Disqus&lt;/li&gt;
&lt;li&gt;  Embed it everywhere in a single JS file; 65kB (20kB gzipped)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of these features are good reasons to choose Isso as your backend comment system instead of one of the others. If you want to install it in a Docker (or Docker Swarm) environment you can follow my personal guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install Isso Comments With Docker
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisite
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Docker (optional Docker Swarm):&lt;/strong&gt; To fully follow this tutorial about installing Isso for your blog you need to have a running Docker environment. I will also provide a Docker Swarm file at the end.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Traefik&lt;/strong&gt;: Traefik is the load balancer that forwards my request to the Docker container. You need to have one installed to access the comments with an URL. If you do not have a running Traefik you will learn &lt;a href="https://www.paulsblog.dev/how-to-setup-traefik-with-automatic-letsencrypt-certificate-resolver/" rel="noopener noreferrer"&gt;within this tutorial&lt;/a&gt; how you can set one up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you don't want to use Docker and Traefik please follow &lt;a href="https://isso-comments.de/docs/reference/installation/" rel="noopener noreferrer"&gt;the official Installation Guide&lt;/a&gt; on the Isso website.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set Up Docker Compose File
&lt;/h3&gt;

&lt;p&gt;If you prepared everything you start installing Isso. First, you have to download the latest version of Isso from the GitHub page: &lt;a href="https://github.com/posativ/isso/" rel="noopener noreferrer"&gt;https://github.com/posativ/isso/&lt;/a&gt;. You can either download the zip file containing the master branch and extract it or clone it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone git@github.com:posativ/isso.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then switch into the &lt;code&gt;isso&lt;/code&gt; folder, delete the docker-compose.yml, create a new docker-compose.yml, and paste the following code snippet into it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.4"&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;isso&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;isso&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GID=1000&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;UID=1000&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db:/db&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik-public&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.enable=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.docker.network=traefik-public&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.constraint-label=traefik-public&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.isso-http.rule=Host(`YOUR_DOMAIN`)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.isso-http.entrypoints=http&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.isso-http.middlewares=https-redirect&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.isso-https.rule=Host(`YOUR_DOMAIN`)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.isso-https.entrypoints=https&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.isso-https.tls=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.isso-https.tls.certresolver=le&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.services.isso.loadbalancer.server.port=8080&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;wget --no-verbose --tries=1 --spider http://localhost:8080/info || exit &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;  &lt;span class="c1"&gt;# short timeout needed during start phase&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;traefik-public&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within &lt;strong&gt;Line 5 - 8&lt;/strong&gt; you can see within this Compose file that the Isso image will be built from the Dockerfile that you downloaded from the official GitHub repository and will be named &lt;code&gt;isso&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Line 12-13 (and 34-35)&lt;/strong&gt;, I defined a volume in which all comments will be saved within an SQLite DB.&lt;/p&gt;

&lt;p&gt;The labels section in &lt;strong&gt;Line 16 - 27&lt;/strong&gt; contains important information for Traefik. Replace YOUR_DOMAIN with the domain where the Isso backend should be accessible. This is important for the moderation of comments.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Line 28 - 33&lt;/strong&gt;, I also added a health check that will check every 5s if the Isso backend is running correctly. To learn more about Docker health checks you should read this tutorial:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.paulsblog.dev/how-to-successfully-implement-a-healthcheck-in-docker-compose/" rel="noopener noreferrer"&gt;https://www.paulsblog.dev/how-to-successfully-implement-a-healthcheck-in-docker-compose/&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration of Isso Backend
&lt;/h3&gt;

&lt;p&gt;Before you can run your Isso backend you have to adjust the Isso configuration file to your needs.&lt;/p&gt;

&lt;p&gt;The following file shows the most important settings that you HAVE to update in order to have a working Isso backend. I will explain them afterward.&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="o"&gt;[&lt;/span&gt;general]
dbpath &lt;span class="o"&gt;=&lt;/span&gt; /db/comments.db
host &lt;span class="o"&gt;=&lt;/span&gt;
    https://www.knulst.de
notify &lt;span class="o"&gt;=&lt;/span&gt; smtp

&lt;span class="o"&gt;[&lt;/span&gt;admin]
enabled &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;password &lt;span class="o"&gt;=&lt;/span&gt; yourSuperSafePasswordThatYouNeedInAdminMenu

&lt;span class="o"&gt;[&lt;/span&gt;moderation]
enabled &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;

&lt;span class="o"&gt;[&lt;/span&gt;server]
listen &lt;span class="o"&gt;=&lt;/span&gt; http://localhost:8080
public-endpoint &lt;span class="o"&gt;=&lt;/span&gt; https://isso.knulst.de

&lt;span class="o"&gt;[&lt;/span&gt;smtp]
username &lt;span class="o"&gt;=&lt;/span&gt; YOUR_EMAIL_USER
password &lt;span class="o"&gt;=&lt;/span&gt; YOUR_EMAIL_USERPASS
host &lt;span class="o"&gt;=&lt;/span&gt; YOUR_EMAIL_HOST
port &lt;span class="o"&gt;=&lt;/span&gt; 587
security &lt;span class="o"&gt;=&lt;/span&gt; starttls
to &lt;span class="o"&gt;=&lt;/span&gt; USER_WHO_SHOULD_RECEIVE_MAIL
from &lt;span class="o"&gt;=&lt;/span&gt; YOUR_EMAIL_USER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;[general]:&lt;/strong&gt; Set the dbpath, the URL of your blog, and that you want to get Emails if new comments are submitted&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[admin]:&lt;/strong&gt; Enable administration and set your admin password. This is needed to login into the admin interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[moderation]:&lt;/strong&gt; Enable moderation of new comments. If set to false every comment will be shown instantly. Not recommended!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[server]:&lt;/strong&gt; Set your public endpoint for your Isso backend. Should be equal to the value within the Compose file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[smtp]:&lt;/strong&gt; Set your Email server settings and account data of your Email.&lt;/p&gt;

&lt;h3&gt;
  
  
  Run Isso Backend
&lt;/h3&gt;

&lt;p&gt;After adjusting the Compose and &lt;code&gt;isso.cfg&lt;/code&gt; file to your needs, you can start the Isso backend by executing:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;After some seconds your Isso backend should be running at the specified domain. You can check it by going to &lt;strong&gt;&lt;a href="https://your-chosen-domain.com/info" rel="noopener noreferrer"&gt;https://your-chosen-domain.com/info&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrate Isso To Ghost CMS (or any equivalent Blog)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Adjust The Source Code Snippet
&lt;/h3&gt;

&lt;p&gt;Now that you have installed your Isso backend you can integrate comments into your blog. To do this, all you have to do is insert the following snippet in your source code. I will explain how it is done in Ghost Blogging software afterward.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;data-isso=&lt;/span&gt;&lt;span class="s"&gt;"//isso.knulst.de/"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"//isso.knulst.de/js/embed.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"isso-thread"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First of all replace &lt;code&gt;isso.knulst.de&lt;/code&gt; with the domain, you used with your Isso bac&lt;br&gt;&lt;br&gt;
kend. Furthermore, you should carefully read the source code snippet because there are two important settings:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; The URL in data-isso starts with two &lt;code&gt;/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; The URL in src also starts with two &lt;code&gt;/&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you change the URLs to your needs keep the two &lt;code&gt;/&lt;/code&gt; in front of the Isso backend URL. This is needed because the Isso backend is deployed on a different host than the blog.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add Snippet To Your Page
&lt;/h3&gt;

&lt;p&gt;With the adjusted source code snippet you can now add comments to your blog. If you use the Ghost Blogging platform you should first download the design that you are using on your instance and extract it into a folder.&lt;/p&gt;

&lt;p&gt;Locate the &lt;code&gt;post.hbs&lt;/code&gt; file and check where the article ends. The best place for comments will be below the Tag List that is often shown at the end of an article. After you found the closing &lt;code&gt;&amp;lt;/div&amp;gt;&lt;/code&gt; insert the snippet in front of that &lt;code&gt;&amp;lt;/div&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Zip your theme, reupload it to your Ghost Blogging Software, and activate it within the Design interface.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you use another blogging software you should also be able to edit the &lt;code&gt;post&lt;/code&gt; html/php/js file and add the snippet where you want to see comments.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now, switch to any article on your blog and you should see the comments section:&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%2Fi3rjczj2bfxf8l0ttfl3.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%2Fi3rjczj2bfxf8l0ttfl3.webp" alt="Picture of Isso comments on your website" width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you set up the Isso backend as I explained in the previous section anyone can now comment on every post and you will be informed by email about a new comment and can instantly activate or delete it within the email:&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%2Fi9aokd4t02jaxib15ozp.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%2Fi9aokd4t02jaxib15ozp.webp" alt="Email content after someone created a comment with Isso" width="705" height="166"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After getting some cool comments they will be displayed below your post. Because activated the Gravatar feature and set the type to &lt;code&gt;robohash&lt;/code&gt;, I have cool robot pictures for every user that creates a comment:&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%2Fzv4e450ym8iimjsuj6u8.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%2Fzv4e450ym8iimjsuj6u8.webp" alt="Picture of comments done with Isso using robohash as image creator" width="800" height="926"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy In Docker Swarm
&lt;/h2&gt;

&lt;p&gt;To deploy Isso into your Docker Swarm you can use &lt;a href="https://ftp.f1nalboss.de/data/docker-compose.isso.yml" rel="noopener noreferrer"&gt;my Isso Docker Compose file&lt;/a&gt; that has the same settings as the plain Compose file. But, this will only work as intended if you set up your Docker Swarm as I describe in this tutorial:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.paulsblog.dev/docker-swarm-in-a-nutshell/" rel="noopener noreferrer"&gt;https://www.paulsblog.dev/docker-swarm-in-a-nutshell/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Furthermore, you need a Traefik Load Balancer in your Docker Swarm that will be used by the Compose file. I explained how to enhance your Docker Swarm with a Traefik in this tutorial:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.paulsblog.dev/services-you-want-to-have-in-a-swarm-environment/#traefik" rel="noopener noreferrer"&gt;https://www.paulsblog.dev/services-you-want-to-have-in-a-swarm-environment/#traefik&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Notes
&lt;/h2&gt;

&lt;p&gt;While many writers argue that you do not need a comment system integrated into your blog I completely deny this argument! I personally think that comments are a great way to interact with your users, answer questions and build your community. Also, if users comment on your articles you could use this interaction to improve your content.&lt;/p&gt;

&lt;p&gt;If you enjoyed reading this article consider commenting your valuable thoughts in the comments section. I would love to hear your feedback about my tutorial. Furthermore, share this article with fellow bloggers to help them get comments on their blogs.&lt;/p&gt;

&lt;p&gt;This article was originally published on my blog at &lt;a href="https://www.paulsblog.dev/how-to-add-comments-to-your-blog-with-docker/" rel="noopener noreferrer"&gt;https://www.paulsblog.dev/how-to-add-comments-to-your-blog-with-docker/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to connect with me on &lt;a href="https://www.paulsblog.dev" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;, &lt;a href="https://medium.knulst.de/" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/paulknulst/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://twitter.com/paulknulst" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;, and &lt;a href="https://github.com/paulknulst" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;🙌 Support this content&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you like this content, please consider &lt;a href="https://www.paulsblog.dev/support/" rel="noopener noreferrer"&gt;supporting me&lt;/a&gt;. You can share it on social media or &lt;a href="https://buymeacoffee.com/paulknulst" rel="noopener noreferrer"&gt;buy me a coffee&lt;/a&gt;! Any support helps!&lt;/p&gt;

&lt;p&gt;Furthermore, you can &lt;a href="https://www.paulsblog.dev/#/portal/signup/free" rel="noopener noreferrer"&gt;sign up for my newsletter&lt;/a&gt; to show your contribution to my content. See the &lt;a href="https://www.paulsblog.dev/contribute/" rel="noopener noreferrer"&gt;contribute page&lt;/a&gt; for all (free or paid) ways to say thank you!&lt;/p&gt;

&lt;p&gt;Thanks! 🥰&lt;/p&gt;




&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@lunarts?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Volodymyr Hryshchenko&lt;/a&gt; / &lt;a href="https://unsplash.com/s/photos/discussion?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Advanced React UI Components To Optimize Development Process</title>
      <dc:creator>Paul Knulst</dc:creator>
      <pubDate>Sun, 12 Feb 2023 19:52:05 +0000</pubDate>
      <link>https://dev.to/paulknulst/advanced-react-ui-components-to-optimize-development-process-2hco</link>
      <guid>https://dev.to/paulknulst/advanced-react-ui-components-to-optimize-development-process-2hco</guid>
      <description>&lt;p&gt;&lt;strong&gt;Power-Charge Your React App Like Never Before With Mantine, TipTap, Syntax-Highlighter, and more&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.paulsblog.dev/" rel="noopener noreferrer"&gt;Paul Knulst&lt;/a&gt;  in  &lt;a href="https://www.paulsblog.dev/tag/javascript/" rel="noopener noreferrer"&gt;JavaScript&lt;/a&gt; • Jul 13, 2022 • 2 min read&lt;/p&gt;




&lt;p&gt;Today, I will bring your attention to 5 lightweight free UI components that can be used to enhance the user experience on your website very easily.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://mantine.dev/" rel="noopener noreferrer"&gt;1. Mantine&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Mantine is a fully-featured React components library that can be used to build fully functional accessible web applications. Furthermore, it includes several customizable React components and hooks with native Dark mode support to cover you in any situation. It is focused on usability, accessibility, and developer experience. Mantine is TypeScript based.&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%2F1bors9ks56skug6qlcrt.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%2F1bors9ks56skug6qlcrt.webp" alt="Screenshot of react component library mantine.dev homepage showing its features." width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mantinedev/mantine" rel="noopener noreferrer"&gt;Find it on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. react-easy-crop
&lt;/h2&gt;

&lt;p&gt;react-easy-crop can be used to crop images/videos with easy interactions. It supports drag, zoom, and rotating images in any format or base64 string. Also, it supports every video format that is supported in HTML5.&lt;/p&gt;

&lt;p&gt;Furthermore, react-easy-crop is a mobile-friendly UI component.&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%2F7i4tr0eezjbh9lgqpk0w.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%2F7i4tr0eezjbh9lgqpk0w.webp" alt="Screenshot of React component react-easy-crop GitHub project taken by pauls dev blog" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ricardo-ch/react-easy-crop" rel="noopener noreferrer"&gt;Find it on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://omgovich.github.io/react-colorful/" rel="noopener noreferrer"&gt;3. react-colorful&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;react-colorful provides a tiny (only 2.8 KB gzipped) color picker component for React apps (also Preact). With absolute no dependencies, it can still be used to have a cross-browser workable, mobile-friendly color picker that is built with hooks and functional components only. Also, the color picker is written in TypeScript and has all types included.&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%2F67aa08egyiapxgbuzcnu.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%2F67aa08egyiapxgbuzcnu.webp" alt="Screenshot of React component react-colorful project website taken by pauls dev blog" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/omgovich/react-colorful" rel="noopener noreferrer"&gt;Find it on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://react-syntax-highlighter.github.io/react-syntax-highlighter/demo/" rel="noopener noreferrer"&gt;4. React Syntax Highlighter&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;React Syntax Highlighter is a React UI component that enables syntax highlighting in any React app by integrating/combining two popular projects: &lt;a href="https://github.com/wooorm/lowlight" rel="noopener noreferrer"&gt;lowlight&lt;/a&gt; and &lt;a href="https://github.com/wooorm/refractor" rel="noopener noreferrer"&gt;refractor&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Highlighting the code is done with Prism.js and highlight.js by using inline styles instead of altering the DOM manually or using &lt;code&gt;dangerouslySetInnerHTML&lt;/code&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%2F83sl8ni8grcn12vtq19q.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%2F83sl8ni8grcn12vtq19q.webp" alt="Screenshot of React Syntax highlighter project website taken by pauls dev blog" width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/react-syntax-highlighter/react-syntax-highlighter" rel="noopener noreferrer"&gt;Find it on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://tiptap.dev/" rel="noopener noreferrer"&gt;5. TipTap&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;TipTap is a headless, framework-agnostic rich text editor that is extendable and based on ProseMirror. It enables full control over every aspect of a text editor experience. Also, it is very customizable, comes with a lot of extensions, and is fully documented.&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%2Fl5dl1l9nmadzp6rgpc3s.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%2Fl5dl1l9nmadzp6rgpc3s.webp" alt="Screenshot of React component TipTap project website taken by pauls dev blog" width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Furthermore, TipTap works with Vanilla JavaScript and is also integrated into several TypeScript-based frameworks (React, Next.js, Vue, etc…).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ueberdosis/tiptap" rel="noopener noreferrer"&gt;Find it on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Notes
&lt;/h2&gt;

&lt;p&gt;I hope you like these UI components and will use them in your next React project.&lt;/p&gt;

&lt;p&gt;Also, if you have any questions, ideas, recommendations, or want to share your own awesome UI component, please jot them down below. I try to answer your question if possible and will test your recommendations.&lt;/p&gt;

&lt;p&gt;This article was originally published on my blog at &lt;a href="https://www.paulsblog.dev/advanced-react-ui-components-to-optimize-development-process/" rel="noopener noreferrer"&gt;https://www.paulsblog.dev/advanced-react-ui-components-to-optimize-development-process/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to connect with me on &lt;a href="https://www.paulsblog.dev" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;, &lt;a href="https://medium.knulst.de/" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/paulknulst/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://twitter.com/paulknulst" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;, and &lt;a href="https://github.com/paulknulst" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;🙌 Support this content&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you like this content, please consider &lt;a href="https://www.paulsblog.dev/support/" rel="noopener noreferrer"&gt;supporting me&lt;/a&gt;. You can share it on social media or &lt;a href="https://buymeacoffee.com/paulknulst" rel="noopener noreferrer"&gt;buy me a coffee&lt;/a&gt;! Any support helps!&lt;/p&gt;

&lt;p&gt;Furthermore, you can &lt;a href="https://www.paulsblog.dev/#/portal/signup/free" rel="noopener noreferrer"&gt;sign up for my newsletter&lt;/a&gt; to show your contribution to my content. See the &lt;a href="https://www.paulsblog.dev/contribute/" rel="noopener noreferrer"&gt;contribute page&lt;/a&gt; for all (free or paid) ways to say thank you!&lt;/p&gt;

&lt;p&gt;Thanks! 🥰&lt;/p&gt;




&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@lautaroandreani?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Lautaro Andreani&lt;/a&gt; / &lt;a href="https://unsplash.com/photos/xkBaqlcqeb4?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>git</category>
      <category>shell</category>
      <category>productivity</category>
      <category>announcement</category>
    </item>
  </channel>
</rss>
