<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Blog - Kais BETTAIEB</title>
    <link>https://blog.kaisbettaieb.dev/</link>
    <description>Python Developer crafting robust applications at the intersection of design, technology, and user experience.</description>
    <pubDate>Tue, 28 Apr 2026 11:03:54 +0000</pubDate>
    <item>
      <title>How to Deploy Next.js on Coolify v4 Using a Custom Dockerfile</title>
      <link>https://blog.kaisbettaieb.dev/how-to-deploy-next-js-on-coolify-v4-using-a-custom-dockerfile</link>
      <description>&lt;![CDATA[#deploy #nextjs #coolify #Docker #github #selfhost&#xA;&#xA;Coolify has emerged as a premier self-hosted alternative to Vercel and Netlify. While its default Nixpacks builder is excellent for zero-configuration deployments, developers often require granular control over the build environment.&#xA;&#xA;This guide demonstrates how to deploy a Next.js application on Coolify v4 (specifically tested on v4.0.0-beta.460) using a multi-stage Dockerfile. This method ensures a highly optimized, production-ready image.&#xA;!--more--&#xA;&#xA;Prerequisites&#xA;&#xA;A running instance of Coolify v4 (Self-hosted).&#xA;A Next.js project pushed to a Git repository (GitHub, GitLab, etc.).&#xA;&#xA;Step 1: Optimize Next.js for Docker&#xA;&#xA;The most critical step before configuring the container is setting up Next.js to create a standalone build. This feature automatically traces your import dependencies and creates a smaller deployment folder, eliminating the need to copy the entire nodemodules directory into your final image.&#xA;&#xA;Open your next.config.js file and add the output: &#39;standalone&#39; configuration:&#xA;&#xA;next.config.js&#xA;/* @type {import(&#39;next&#39;).NextConfig} /&#xA;const nextConfig = {&#xA;  output: &#34;standalone&#34;,&#xA;};&#xA;&#xA;module.exports = nextConfig;&#xA;&#xA;Step 2: Create the Multi-Stage Dockerfile&#xA;&#xA;Create a file named Dockerfile in the root of your project. We will use a multi-stage build strategy to keep the final image lightweight. This process splits the build into three stages:&#xA;Deps: Installs dependencies (cached for speed).&#xA;Builder: Compiles the Next.js application.&#xA;Runner: The final, minimal production image.&#xA;&#xA;Copy the following code into your Dockerfile:&#xA;&#xA;Dockerfile&#xA;Stage 1: Install dependencies&#xA;FROM node:18-alpine AS deps&#xA;WORKDIR /app&#xA;Install libc6-compat (needed for some native dependencies)&#xA;RUN apk add --no-cache libc6-compat&#xA;COPY package.json package-lock.json ./&#xA;RUN npm ci&#xA;&#xA;Stage 2: Build the application&#xA;FROM node:18-alpine AS builder&#xA;WORKDIR /app&#xA;COPY --from=deps /app/nodemodules ./nodemodules&#xA;COPY . .&#xA;Disable Next.js telemetry during build&#xA;ENV NEXTTELEMETRYDISABLED 1&#xA;RUN npm run build&#xA;&#xA;Stage 3: Production runner&#xA;FROM node:18-alpine AS runner&#xA;WORKDIR /app&#xA;ENV NODEENV production&#xA;ENV NEXTTELEMETRYDISABLED 1&#xA;&#xA;Create a system user for security&#xA;RUN addgroup --system --gid 1001 nodejs&#xA;RUN adduser --system --uid 1001 nextjs&#xA;&#xA;Copy the standalone build and static assets&#xA;COPY --from=builder /app/public ./public&#xA;COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./&#xA;COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static&#xA;&#xA;USER nextjs&#xA;EXPOSE 3000&#xA;ENV PORT 3000&#xA;CMD [&#34;node&#34;, &#34;server.js&#34;]&#xA;&#xA;  Note: Create a .dockerignore file in your root directory and add nodemodules and .next to it. This prevents local build files from slowing down the Docker build context.&#xA;&#xA;Step 3: Configure the Project in Coolify&#xA;&#xA;Log in to your Coolify dashboard.&#xA;Click + New Resource and select Public Repository (or Private Repository if you have configured GitHub App access).&#xA;Paste your repository URL and select the branch you wish to deploy (usually main).&#xA;Under Build Pack, select Dockerfile.&#xA;    Coolify defaults to Nixpacks, so this manual selection is crucial.*&#xA;Click Continue.&#xA;&#xA;Step 4: Define Environment Variables&#xA;&#xA;If your Next.js application relies on environment variables (like DATABASEURL or APIKEY), you must define them in Coolify before deploying.&#xA;&#xA;Navigate to the Environment Variables tab in your project view.&#xA;Add your keys and values.&#xA;Ensure you do not commit a .env file to your Git repository; managing them inside Coolify is more secure.&#xA;&#xA;Step 5: Deploy&#xA;&#xA;Go back to the Configuration tab.&#xA;Verify the Ports Exposes setting is set to 3000 (matching the EXPOSE instruction in our Dockerfile).&#xA;Click Deploy in the top right corner.&#xA;&#xA;Coolify will now pull your code, build the Docker image using the stages defined in Step 2, and start the container. You can view the build logs in real-time to monitor the progress. Once the status changes to Running, your optimized Next.js application is live.&#xA;&#xA;div class=&#34;blog-signature&#34;&#xD;&#xA;    div class=&#34;sig-content&#34;&#xD;&#xA;        pThanks for reading! If you found this helpful:/p&#xD;&#xA;        div class=&#34;sig-links&#34;&#xD;&#xA;            a href=&#34;https://ko-fi.com/kaisbettaieb&#34; class=&#34;kofi-btn&#34; target=&#34;blank&#34; rel=&#34;noopener&#34;☕ Buy me a coffee/a&#xD;&#xA;            span class=&#34;divider&#34;or/span&#xD;&#xA;            a href=&#34;https://github.com/kaisbettaieb&#34; class=&#34;text-link&#34;Check my Code/a&#xD;&#xA;            span class=&#34;divider&#34;or/span&#xD;&#xA;            a href=&#34;https://kbt.dev&#34; class=&#34;text-link&#34;More about me/a&#xD;&#xA;        /div&#xD;&#xA;    /div&#xD;&#xA;/div&#xD;&#xA;div id=&#34;comment-section&#34;/div&#xD;&#xA;&#xD;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="https://blog.kaisbettaieb.dev/tag:deploy" class="hashtag"><span>#</span><span class="p-category">deploy</span></a> <a href="https://blog.kaisbettaieb.dev/tag:nextjs" class="hashtag"><span>#</span><span class="p-category">nextjs</span></a> <a href="https://blog.kaisbettaieb.dev/tag:coolify" class="hashtag"><span>#</span><span class="p-category">coolify</span></a> <a href="https://blog.kaisbettaieb.dev/tag:Docker" class="hashtag"><span>#</span><span class="p-category">Docker</span></a> <a href="https://blog.kaisbettaieb.dev/tag:github" class="hashtag"><span>#</span><span class="p-category">github</span></a> <a href="https://blog.kaisbettaieb.dev/tag:selfhost" class="hashtag"><span>#</span><span class="p-category">selfhost</span></a></p>

<p>Coolify has emerged as a premier self-hosted alternative to Vercel and Netlify. While its default Nixpacks builder is excellent for zero-configuration deployments, developers often require granular control over the build environment.</p>

<p>This guide demonstrates how to deploy a Next.js application on <strong>Coolify v4</strong> (specifically tested on v4.0.0-beta.460) using a multi-stage Dockerfile. This method ensures a highly optimized, production-ready image.
</p>

<h3 id="prerequisites">Prerequisites</h3>
<ul><li>A running instance of <strong>Coolify v4</strong> (Self-hosted).</li>
<li>A Next.js project pushed to a Git repository (GitHub, GitLab, etc.).</li></ul>

<h3 id="step-1-optimize-next-js-for-docker">Step 1: Optimize Next.js for Docker</h3>

<p>The most critical step before configuring the container is setting up Next.js to create a standalone build. This feature automatically traces your import dependencies and creates a smaller deployment folder, eliminating the need to copy the entire <code>node_modules</code> directory into your final image.</p>

<p>Open your <code>next.config.js</code> file and add the <code>output: &#39;standalone&#39;</code> configuration:</p>

<p><strong>next.config.js</strong></p>

<pre><code class="language-javascript">/** @type {import(&#39;next&#39;).NextConfig} */
const nextConfig = {
  output: &#34;standalone&#34;,
};

module.exports = nextConfig;
</code></pre>

<h3 id="step-2-create-the-multi-stage-dockerfile">Step 2: Create the Multi-Stage Dockerfile</h3>

<p>Create a file named <code>Dockerfile</code> in the root of your project. We will use a multi-stage build strategy to keep the final image lightweight. This process splits the build into three stages:
1.  <strong>Deps:</strong> Installs dependencies (cached for speed).
2.  <strong>Builder:</strong> Compiles the Next.js application.
3.  <strong>Runner:</strong> The final, minimal production image.</p>

<p>Copy the following code into your <code>Dockerfile</code>:</p>

<p><strong>Dockerfile</strong></p>

<pre><code class="language-dockerfile"># Stage 1: Install dependencies
FROM node:18-alpine AS deps
WORKDIR /app
# Install libc6-compat (needed for some native dependencies)
RUN apk add --no-cache libc6-compat
COPY package.json package-lock.json* ./
RUN npm ci

# Stage 2: Build the application
FROM node:18-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Disable Next.js telemetry during build
ENV NEXT_TELEMETRY_DISABLED 1
RUN npm run build

# Stage 3: Production runner
FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1

# Create a system user for security
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# Copy the standalone build and static assets
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000
ENV PORT 3000
CMD [&#34;node&#34;, &#34;server.js&#34;]
</code></pre>

<blockquote><p><strong>Note:</strong> Create a <code>.dockerignore</code> file in your root directory and add <code>node_modules</code> and <code>.next</code> to it. This prevents local build files from slowing down the Docker build context.</p></blockquote>

<h3 id="step-3-configure-the-project-in-coolify">Step 3: Configure the Project in Coolify</h3>
<ol><li>Log in to your Coolify dashboard.</li>
<li>Click <strong>+ New Resource</strong> and select <strong>Public Repository</strong> (or <strong>Private Repository</strong> if you have configured GitHub App access).</li>
<li>Paste your repository URL and select the branch you wish to deploy (usually <code>main</code>).</li>
<li>Under <strong>Build Pack</strong>, select <strong>Dockerfile</strong>.
<ul><li><em>Coolify defaults to Nixpacks, so this manual selection is crucial.</em></li></ul></li>
<li>Click <strong>Continue</strong>.</li></ol>

<h3 id="step-4-define-environment-variables">Step 4: Define Environment Variables</h3>

<p>If your Next.js application relies on environment variables (like <code>DATABASE_URL</code> or <code>API_KEY</code>), you must define them in Coolify before deploying.</p>
<ol><li>Navigate to the <strong>Environment Variables</strong> tab in your project view.</li>
<li>Add your keys and values.</li>
<li>Ensure you do <strong>not</strong> commit a <code>.env</code> file to your Git repository; managing them inside Coolify is more secure.</li></ol>

<h3 id="step-5-deploy">Step 5: Deploy</h3>
<ol><li>Go back to the <strong>Configuration</strong> tab.</li>
<li>Verify the <strong>Ports Exposes</strong> setting is set to <code>3000</code> (matching the <code>EXPOSE</code> instruction in our Dockerfile).</li>
<li>Click <strong>Deploy</strong> in the top right corner.</li></ol>

<p>Coolify will now pull your code, build the Docker image using the stages defined in Step 2, and start the container. You can view the build logs in real-time to monitor the progress. Once the status changes to <strong>Running</strong>, your optimized Next.js application is live.</p>

<div class="blog-signature">
    <div class="sig-content">
        <p>Thanks for reading! If you found this helpful:</p>
        <div class="sig-links">
            <a href="https://ko-fi.com/kaisbettaieb" class="kofi-btn" target="_blank">☕ Buy me a coffee</a>
            <span class="divider">or</span>
            <a href="https://github.com/kaisbettaieb" class="text-link">Check my Code</a>
            <span class="divider">or</span>
            <a href="https://kbt.dev" class="text-link">More about me</a>
        </div>
    </div>
</div>
<div id="comment-section"></div>
]]></content:encoded>
      <guid>https://blog.kaisbettaieb.dev/how-to-deploy-next-js-on-coolify-v4-using-a-custom-dockerfile</guid>
      <pubDate>Fri, 16 Jan 2026 00:51:24 +0000</pubDate>
    </item>
    <item>
      <title>Adding Comments to WriteFreely with Cusdis</title>
      <link>https://blog.kaisbettaieb.dev/adding-comments-to-writefreely-with-cusdis</link>
      <description>&lt;![CDATA[#cusdis #comments #writefreely #javascript &#xA;&#xA;WriteFreely is beloved for its minimalism. However, fostering a community often requires a feedback loop. The challenge is adding comments without shattering that clean, distraction-free aesthetic.&#xA;!--more--&#xA;I recently integrated Cusdis—a lightweight, open-source, and privacy-friendly comment system—into this blog. It aligns seamlessly with WriteFreely’s ethos: it requires no user tracking and adapts beautifully with a bit of custom CSS.&#xA;&#xA;Here is how to implement a fully dynamic, styled comment section. This script ensures the widget loads only on individual posts (keeping your homepage pristine) and automatically respects dark mode.&#xA;&#xA;Step 1: Get Your Cusdis ID&#xA;&#xA;First, you need a running Cusdis instance. You have two paths:&#xA;&#xA;Cloud: Create a free account at Cusdis.com.&#xA;Self-Hosted: Host it yourself on a VPS (using Coolify or Docker) for 100% data ownership.&#xA;&#xA;Once you have access to your dashboard, copy your App ID (e.g., f42fd160-a4f2-...). You will need this for the JavaScript configuration below.&#xA;&#xA;Step 2: The Logic (Why Custom JS?)&#xA;&#xA;Since WriteFreely lacks native support for editing individual post templates, we cannot simply paste the standard Cusdis embed code. A static embed cannot automatically fetch the unique Page ID or Title for every article.&#xA;&#xA;Instead, we use Custom JavaScript to:&#xA;&#xA;Verify context: Check if the user is viewing a single post (ignoring the homepage).&#xA;Build the UI: Dynamically append a &#34;Comments&#34; container to the bottom of the article.&#xA;Inject the Widget: Initialize Cusdis with the correct metadata (ID, URL, Title).&#xA;Style: Force the widget into &#34;Dark Mode&#34; to match the theme.&#xA;&#xA;Step 3: The JavaScript Code&#xA;&#xA;Navigate to your WriteFreely dashboard, go to Customize   Custom JavaScript, and paste the code below.&#xA;&#xA;  Note: Be sure to replace &#39;YOUR-APP-ID-HERE&#39; with the actual ID you copied in Step 1.&#xA;&#xA;(function() {&#xA;    // Only run this script if we are on a post page (body#post)&#xA;    // This prevents the comments from loading on the homepage or collections.&#xA;    var postBody = document.querySelector(&#39;body#post&#39;);&#xA;    &#xA;    if (postBody) {&#xA;        // 1. Create the container div for Cusdis&#xA;        var cusdisContainer = document.createElement(&#39;div&#39;);&#xA;        cusdisContainer.id = &#39;cusdisthread&#39;;&#xA;        cusdisContainer.dataset.host = &#39;https://cusdis.com&#39;;&#xA;        cusdisContainer.dataset.appId = &#39;YOUR-APP-ID-HERE&#39;; // PASTE YOUR ID HERE&#xA;        cusdisContainer.dataset.pageId = window.location.pathname;&#xA;        cusdisContainer.dataset.pageUrl = window.location.href;&#xA;        cusdisContainer.dataset.pageTitle = document.title;&#xA;        cusdisContainer.dataset.theme = &#39;dark&#39;; // Forces dark mode&#xA;&#xA;        // 2. Append the container to the article or main content area&#xA;        // Note: &#39;article&#39; is standard in most WriteFreely themes. &#xA;        var article = document.querySelector(&#39;article&#39;);&#xA;        if (article) {&#xA;            article.appendChild(cusdisContainer);&#xA;        }&#xA;&#xA;        // 3. Load the Cusdis SDK asynchronously&#xA;        var script = document.createElement(&#39;script&#39;);&#xA;        script.src = &#39;https://cusdis.com/js/cusdis.es.js&#39;;&#xA;        script.async = true;&#xA;        script.defer = true;&#xA;        document.body.appendChild(script);&#xA;    }&#xA;})();&#xA;&#xA;Step 4: The styling&#xA;&#xA;Now that the functionality is in place, let&#39;s style the container to look like a piece of retro-futuristic hardware. We want a monospace font, a &#34;console&#34; border, and a distinct separation from the rest of your content.&#xA;&#xA;Navigate to Customize   Custom CSS and add the following:&#xA;&#xA;/ --- VISIBILITY RULES of COMMENT SECTION --- /&#xA;body#collection #comment-section,&#xA;body#subpage #comment-section {&#xA;    display: none !important;&#xA;}&#xA;&#xA;body#post #comment-section {&#xA;    display: block !important;&#xA;}&#xA;&#xA;/ --- END VISIBILITY RULES OF COMMENT SECTION -- /&#xA;/ --- CUSDIS COMMENT SECTION FIXES --- /&#xA;comment-section {&#xA;    margin-top: 3rem; / space from article content /&#xA;    width: 100%;&#xA;    max-width: 100%;&#xA;}&#xA;&#xA;/ Cusdis iframe container /&#xA;cusdisthread {&#xA;    width: 100% !important;&#xA;    min-height: 500px; / initial height /&#xA;    border-radius: 8px;&#xA;    overflow: hidden; / remove inner scrollbars /&#xA;    background-color: var(--surface-color) !important;&#xA;    border: 1px solid var(--border-color) !important;&#xA;    box-shadow: 0 0 15px rgba(0, 0, 0, 0.4);&#xA;}&#xA;&#xA;/ Make the iframe responsive /&#xA;cusdisthread iframe {&#xA;    width: 100% !important;&#xA;    min-height: 500px !important;&#xA;    height: auto !important;&#xA;    border: none !important;&#xA;}&#xA;&#xA;/ Dark theme for Cusdis content /&#xA;cusdisthread {&#xA;    --cusdis-background: var(--surface-color);&#xA;    --cusdis-text-color: var(--text-color);&#xA;    --cusdis-accent-color: var(--accent-color);&#xA;    --cusdis-border-color: var(--border-color);&#xA;    --cusdis-input-background: #222; / input background /&#xA;    --cusdis-input-text-color: #eee; / input text /&#xA;    --cusdis-button-background: var(--accent-color);&#xA;    --cusdis-button-text-color: #000;&#xA;}&#xA;&#xA;/ Force Cusdis to expand to fit content (if Cusdis JS supports it) /&#xA;cusdisthread iframe {&#xA;    height: 100% !important;&#xA;}&#xA;&#xA;/ Inputs and textareas /&#xA;cusdisthread textarea,&#xA;cusdisthread input {&#xA;    background-color: #222 !important;&#xA;    color: #eee !important;&#xA;    border: 1px solid var(--border-color) !important;&#xA;    border-radius: 6px;&#xA;    padding: 8px;&#xA;}&#xA;&#xA;/ Submit button /&#xA;cusdisthread button {&#xA;    background-color: var(--accent-color) !important;&#xA;    color: #000 !important;&#xA;    font-weight: 700;&#xA;    border-radius: 6px !important;&#xA;    padding: 6px 14px !important;&#xA;    border: none !important;&#xA;    cursor: pointer;&#xA;    transition: all 0.2s ease;&#xA;}&#xA;&#xA;cusdisthread button:hover {&#xA;    background-color: #3aa8d1 !important;&#xA;    box-shadow: 0 0 12px rgba(98, 200, 243, 0.5) !important;&#xA;    color: #000 !important;&#xA;}&#xA;&#xA;/ Scrollbar styling for iframe if it still scrolls /&#xA;cusdisthread iframe::-webkit-scrollbar {&#xA;    width: 8px;&#xA;}&#xA;&#xA;cusdisthread iframe::-webkit-scrollbar-thumb {&#xA;    background-color: #555;&#xA;    border-radius: 4px;&#xA;}&#xA;&#xA;cusdisthread iframe::-webkit-scrollbar-track {&#xA;    background-color: #111;&#xA;}&#xA;&#xA;/ Optional: smooth font for Cusdis /&#xA;cusdisthread,&#xA;cusdisthread iframe {&#xA;    font-family: &#39;Nunito&#39;, sans-serif !important;&#xA;}&#xA;&#xA;Final Thoughts&#xA;&#xA;And that is it. You now have a lightweight, privacy-respecting comment section that loads exclusively on your articles.&#xA;&#xA;By injecting the widget via JavaScript, you bypass WriteFreely&#39;s template limitations while maintaining total control over the look and feel. Your readers can now interact with your content in a space that feels like a natural extension of your blog&#39;s &#34;hacker&#34; aesthetic—no tracking pixels attached.&#xA;&#xA;div class=&#34;blog-signature&#34;&#xD;&#xA;    div class=&#34;sig-content&#34;&#xD;&#xA;        pThanks for reading! If you found this helpful:/p&#xD;&#xA;        div class=&#34;sig-links&#34;&#xD;&#xA;            a href=&#34;https://ko-fi.com/kaisbettaieb&#34; class=&#34;kofi-btn&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;☕ Buy me a coffee/a&#xD;&#xA;            span class=&#34;divider&#34;or/span&#xD;&#xA;            a href=&#34;https://github.com/kaisbettaieb&#34; class=&#34;text-link&#34;Check my Code/a&#xD;&#xA;            span class=&#34;divider&#34;or/span&#xD;&#xA;            a href=&#34;https://kbt.dev&#34; class=&#34;text-link&#34;More about me/a&#xD;&#xA;        /div&#xD;&#xA;    /div&#xD;&#xA;/div&#xD;&#xA;div id=&#34;comment-section&#34;/div&#xD;&#xA;&#xD;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="https://blog.kaisbettaieb.dev/tag:cusdis" class="hashtag"><span>#</span><span class="p-category">cusdis</span></a> <a href="https://blog.kaisbettaieb.dev/tag:comments" class="hashtag"><span>#</span><span class="p-category">comments</span></a> <a href="https://blog.kaisbettaieb.dev/tag:writefreely" class="hashtag"><span>#</span><span class="p-category">writefreely</span></a> <a href="https://blog.kaisbettaieb.dev/tag:javascript" class="hashtag"><span>#</span><span class="p-category">javascript</span></a></p>

<p>WriteFreely is beloved for its minimalism. However, fostering a community often requires a feedback loop. The challenge is adding comments without shattering that clean, distraction-free aesthetic.

I recently integrated <strong>Cusdis</strong>—a lightweight, open-source, and privacy-friendly comment system—into this blog. It aligns seamlessly with WriteFreely’s ethos: it requires no user tracking and adapts beautifully with a bit of custom CSS.</p>

<p>Here is how to implement a fully dynamic, styled comment section. This script ensures the widget loads <strong>only</strong> on individual posts (keeping your homepage pristine) and automatically respects dark mode.</p>

<h2 id="step-1-get-your-cusdis-id">Step 1: Get Your Cusdis ID</h2>

<p>First, you need a running Cusdis instance. You have two paths:</p>
<ul><li><strong>Cloud:</strong> Create a free account at <a href="https://cusdis.com">Cusdis.com</a>.</li>
<li><strong>Self-Hosted:</strong> Host it yourself on a VPS (using Coolify or Docker) for 100% data ownership.</li></ul>

<p>Once you have access to your dashboard, copy your <strong>App ID</strong> (e.g., <code>f42fd160-a4f2-...</code>). You will need this for the JavaScript configuration below.</p>

<h2 id="step-2-the-logic-why-custom-js">Step 2: The Logic (Why Custom JS?)</h2>

<p>Since WriteFreely lacks native support for editing individual post templates, we cannot simply paste the standard Cusdis embed code. A static embed cannot automatically fetch the unique Page ID or Title for every article.</p>

<p>Instead, we use Custom JavaScript to:</p>
<ol><li><strong>Verify context:</strong> Check if the user is viewing a single post (ignoring the homepage).</li>
<li><strong>Build the UI:</strong> Dynamically append a “Comments” container to the bottom of the article.</li>
<li><strong>Inject the Widget:</strong> Initialize Cusdis with the correct metadata (ID, URL, Title).</li>
<li><strong>Style:</strong> Force the widget into “Dark Mode” to match the theme.</li></ol>

<h2 id="step-3-the-javascript-code">Step 3: The JavaScript Code</h2>

<p>Navigate to your WriteFreely dashboard, go to <strong>Customize &gt; Custom JavaScript</strong>, and paste the code below.</p>

<blockquote><p><strong>Note:</strong> Be sure to replace <code>&#39;YOUR-APP-ID-HERE&#39;</code> with the actual ID you copied in Step 1.</p></blockquote>

<pre><code class="language-javascript">(function() {
    // Only run this script if we are on a post page (body#post)
    // This prevents the comments from loading on the homepage or collections.
    var postBody = document.querySelector(&#39;body#post&#39;);
    
    if (postBody) {
        // 1. Create the container div for Cusdis
        var cusdisContainer = document.createElement(&#39;div&#39;);
        cusdisContainer.id = &#39;cusdis_thread&#39;;
        cusdisContainer.dataset.host = &#39;[https://cusdis.com](https://cusdis.com)&#39;;
        cusdisContainer.dataset.appId = &#39;YOUR-APP-ID-HERE&#39;; // PASTE YOUR ID HERE
        cusdisContainer.dataset.pageId = window.location.pathname;
        cusdisContainer.dataset.pageUrl = window.location.href;
        cusdisContainer.dataset.pageTitle = document.title;
        cusdisContainer.dataset.theme = &#39;dark&#39;; // Forces dark mode

        // 2. Append the container to the article or main content area
        // Note: &#39;article&#39; is standard in most WriteFreely themes. 
        var article = document.querySelector(&#39;article&#39;);
        if (article) {
            article.appendChild(cusdisContainer);
        }

        // 3. Load the Cusdis SDK asynchronously
        var script = document.createElement(&#39;script&#39;);
        script.src = &#39;[https://cusdis.com/js/cusdis.es.js](https://cusdis.com/js/cusdis.es.js)&#39;;
        script.async = true;
        script.defer = true;
        document.body.appendChild(script);
    }
})();
</code></pre>

<h2 id="step-4-the-styling">Step 4: The styling</h2>

<p>Now that the functionality is in place, let&#39;s style the container to look like a piece of retro-futuristic hardware. We want a monospace font, a “console” border, and a distinct separation from the rest of your content.</p>

<p>Navigate to <strong>Customize &gt; Custom CSS</strong> and add the following:</p>

<pre><code class="language-css">
/* --- VISIBILITY RULES of COMMENT SECTION --- */
body#collection #comment-section,
body#subpage #comment-section {
    display: none !important;
}

body#post #comment-section {
    display: block !important;
}

/* --- END VISIBILITY RULES OF COMMENT SECTION -- */
/* --- CUSDIS COMMENT SECTION FIXES --- */
#comment-section {
    margin-top: 3rem; /* space from article content */
    width: 100%;
    max-width: 100%;
}

/* Cusdis iframe container */
#cusdis_thread {
    width: 100% !important;
    min-height: 500px; /* initial height */
    border-radius: 8px;
    overflow: hidden; /* remove inner scrollbars */
    background-color: var(--surface-color) !important;
    border: 1px solid var(--border-color) !important;
    box-shadow: 0 0 15px rgba(0, 0, 0, 0.4);
}

/* Make the iframe responsive */
#cusdis_thread iframe {
    width: 100% !important;
    min-height: 500px !important;
    height: auto !important;
    border: none !important;
}

/* Dark theme for Cusdis content */
#cusdis_thread {
    --cusdis-background: var(--surface-color);
    --cusdis-text-color: var(--text-color);
    --cusdis-accent-color: var(--accent-color);
    --cusdis-border-color: var(--border-color);
    --cusdis-input-background: #222; /* input background */
    --cusdis-input-text-color: #eee; /* input text */
    --cusdis-button-background: var(--accent-color);
    --cusdis-button-text-color: #000;
}

/* Force Cusdis to expand to fit content (if Cusdis JS supports it) */
#cusdis_thread iframe {
    height: 100% !important;
}

/* Inputs and textareas */
#cusdis_thread textarea,
#cusdis_thread input {
    background-color: #222 !important;
    color: #eee !important;
    border: 1px solid var(--border-color) !important;
    border-radius: 6px;
    padding: 8px;
}

/* Submit button */
#cusdis_thread button {
    background-color: var(--accent-color) !important;
    color: #000 !important;
    font-weight: 700;
    border-radius: 6px !important;
    padding: 6px 14px !important;
    border: none !important;
    cursor: pointer;
    transition: all 0.2s ease;
}

#cusdis_thread button:hover {
    background-color: #3aa8d1 !important;
    box-shadow: 0 0 12px rgba(98, 200, 243, 0.5) !important;
    color: #000 !important;
}

/* Scrollbar styling for iframe if it still scrolls */
#cusdis_thread iframe::-webkit-scrollbar {
    width: 8px;
}

#cusdis_thread iframe::-webkit-scrollbar-thumb {
    background-color: #555;
    border-radius: 4px;
}

#cusdis_thread iframe::-webkit-scrollbar-track {
    background-color: #111;
}

/* Optional: smooth font for Cusdis */
#cusdis_thread,
#cusdis_thread iframe {
    font-family: &#39;Nunito&#39;, sans-serif !important;
}

</code></pre>

<h2 id="final-thoughts">Final Thoughts</h2>

<p>And that is it. You now have a lightweight, privacy-respecting comment section that loads exclusively on your articles.</p>

<p>By injecting the widget via JavaScript, you bypass WriteFreely&#39;s template limitations while maintaining total control over the look and feel. Your readers can now interact with your content in a space that feels like a natural extension of your blog&#39;s “hacker” aesthetic—no tracking pixels attached.</p>

<div class="blog-signature">
    <div class="sig-content">
        <p>Thanks for reading! If you found this helpful:</p>
        <div class="sig-links">
            <a href="https://ko-fi.com/kaisbettaieb" class="kofi-btn" target="_blank">☕ Buy me a coffee</a>
            <span class="divider">or</span>
            <a href="https://github.com/kaisbettaieb" class="text-link">Check my Code</a>
            <span class="divider">or</span>
            <a href="https://kbt.dev" class="text-link">More about me</a>
        </div>
    </div>
</div>
<div id="comment-section"></div>
]]></content:encoded>
      <guid>https://blog.kaisbettaieb.dev/adding-comments-to-writefreely-with-cusdis</guid>
      <pubDate>Tue, 13 Jan 2026 14:51:59 +0000</pubDate>
    </item>
    <item>
      <title>Deploying WriteFreely on a Coolify VPS with MySQL</title>
      <link>https://blog.kaisbettaieb.dev/deploying-writefreely-on-a-coolify-vps-with-mysql</link>
      <description>&lt;![CDATA[#coolify #writefreely #vps #mysql&#xA;&#xA;I currently run a Coolify instance on my VPS and recently decided to host a lightweight blogging platform. I managed to deploy WriteFreely using the open-source algernon/writefreely Docker image. While the initial deployment was straightforward, configuring the storage and database correctly required some specific steps. Here is a walkthrough of how I achieved a stable setup.&#xA;!--more--&#xA;&#xA;1. The Docker Configuration&#xA;&#xA;First, I pulled the Docker image and deployed it via Coolify. The primary challenge was configuring persistent storage. By default, if you restart the container, you lose your configuration, which makes the service unusable in production.&#xA;&#xA;To address this, I configured Coolify to create a persistent volume. I mapped a volume named wfdata to /data inside the container. This step is critical because the config.ini file needs to reside in this directory to persist across restarts.&#xA;&#xA;2. Adding a Standalone MySQL Database&#xA;&#xA;I preferred not to use the default SQLite engine, so I decided to provision a standalone MySQL database service within Coolify. The process is remarkably straightforward: you simply create a new resource, select MySQL, and let Coolify handle the password generation.&#xA;&#xA;The tricky part was establishing the connection between the app and the database. I initially encountered &#34;Access Denied&#34; errors because I was attempting to use the default mysql user. Once I updated the credentials in the configuration file to use the root user (or a dedicated user with proper permissions), the connection worked perfectly.&#xA;&#xA;3. Linking the Components&#xA;&#xA;To ensure WriteFreely correctly reads from the persistent volume, I had to modify the startup command. I couldn&#39;t immediately locate the command field in the UI, so I utilized the Docker Compose configuration to override the entry point:&#xA;&#xA;/bin/sh -c &#34;./writefreely -c /data/config.ini&#34;&#xA;&#xA;Additionally, I ran the configuration wizard directly inside the terminal to generate the initial config.ini. I made sure to point the encryption keys and database paths specifically to /data.&#xA;&#xA;Conclusion&#xA;&#xA;I now have a fully functional WriteFreely instance running on my VPS, complete with persistent storage and a robust MySQL backend. It is a highly efficient and self-contained setup.&#xA;&#xA;div class=&#34;blog-signature&#34;&#xD;&#xA;    div class=&#34;sig-content&#34;&#xD;&#xA;        pThanks for reading! If you found this helpful:/p&#xD;&#xA;        div class=&#34;sig-links&#34;&#xD;&#xA;            a href=&#34;https://ko-fi.com/kaisbettaieb&#34; class=&#34;kofi-btn&#34; target=&#34;blank&#34; rel=&#34;noopener&#34;☕ Buy me a coffee/a&#xD;&#xA;            span class=&#34;divider&#34;or/span&#xD;&#xA;            a href=&#34;https://github.com/kaisbettaieb&#34; class=&#34;text-link&#34;Check my Code/a&#xD;&#xA;            span class=&#34;divider&#34;or/span&#xD;&#xA;            a href=&#34;https://kbt.dev&#34; class=&#34;text-link&#34;More about me/a&#xD;&#xA;        /div&#xD;&#xA;    /div&#xD;&#xA;/div&#xD;&#xA;div id=&#34;comment-section&#34;/div&#xD;&#xA;&#xD;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="https://blog.kaisbettaieb.dev/tag:coolify" class="hashtag"><span>#</span><span class="p-category">coolify</span></a> <a href="https://blog.kaisbettaieb.dev/tag:writefreely" class="hashtag"><span>#</span><span class="p-category">writefreely</span></a> <a href="https://blog.kaisbettaieb.dev/tag:vps" class="hashtag"><span>#</span><span class="p-category">vps</span></a> <a href="https://blog.kaisbettaieb.dev/tag:mysql" class="hashtag"><span>#</span><span class="p-category">mysql</span></a></p>

<p>I currently run a <strong>Coolify</strong> instance on my VPS and recently decided to host a lightweight blogging platform. I managed to deploy <strong>WriteFreely</strong> using the open-source <code>algernon/writefreely</code> Docker image. While the initial deployment was straightforward, configuring the storage and database correctly required some specific steps. Here is a walkthrough of how I achieved a stable setup.
</p>

<h2 id="1-the-docker-configuration">1. The Docker Configuration</h2>

<p>First, I pulled the Docker image and deployed it via Coolify. The primary challenge was configuring <strong>persistent storage</strong>. By default, if you restart the container, you lose your configuration, which makes the service unusable in production.</p>

<p>To address this, I configured Coolify to create a persistent volume. I mapped a volume named <code>wf_data</code> to <code>/data</code> inside the container. This step is critical because the <code>config.ini</code> file needs to reside in this directory to persist across restarts.</p>

<h2 id="2-adding-a-standalone-mysql-database">2. Adding a Standalone MySQL Database</h2>

<p>I preferred not to use the default SQLite engine, so I decided to <strong>provision a standalone MySQL database service</strong> within Coolify. The process is remarkably straightforward: you simply create a new resource, select MySQL, and let Coolify handle the password generation.</p>

<p>The tricky part was establishing the connection between the app and the database. I initially encountered “Access Denied” errors because I was attempting to use the default <code>mysql</code> user. Once I updated the credentials in the configuration file to use the <code>root</code> user (or a dedicated user with proper permissions), the connection worked perfectly.</p>

<h2 id="3-linking-the-components">3. Linking the Components</h2>

<p>To ensure WriteFreely correctly reads from the persistent volume, I had to modify the startup command. I couldn&#39;t immediately locate the command field in the UI, so I utilized the <strong>Docker Compose</strong> configuration to override the entry point:</p>

<pre><code class="language-bash">/bin/sh -c &#34;./writefreely -c /data/config.ini&#34;
</code></pre>

<p>Additionally, I ran the configuration wizard directly inside the terminal to generate the initial <code>config.ini</code>. I made sure to point the encryption keys and database paths specifically to <code>/data</code>.</p>

<h2 id="conclusion">Conclusion</h2>

<p>I now have a fully functional WriteFreely instance running on my VPS, complete with persistent storage and a robust MySQL backend. It is a highly efficient and self-contained setup.</p>

<div class="blog-signature">
    <div class="sig-content">
        <p>Thanks for reading! If you found this helpful:</p>
        <div class="sig-links">
            <a href="https://ko-fi.com/kaisbettaieb" class="kofi-btn" target="_blank">☕ Buy me a coffee</a>
            <span class="divider">or</span>
            <a href="https://github.com/kaisbettaieb" class="text-link">Check my Code</a>
            <span class="divider">or</span>
            <a href="https://kbt.dev" class="text-link">More about me</a>
        </div>
    </div>
</div>
<div id="comment-section"></div>
]]></content:encoded>
      <guid>https://blog.kaisbettaieb.dev/deploying-writefreely-on-a-coolify-vps-with-mysql</guid>
      <pubDate>Fri, 09 Jan 2026 09:27:18 +0000</pubDate>
    </item>
  </channel>
</rss>