How to Build an Electron Web App with Nativefier and Gitea CI/CD

Introduction

If you spend a significant amount of your day navigating the web, you know the struggle: browser tabs multiply, and essential web applications get lost in the clutter. I recently ran into this exact issue with Zocial, my personal fork of the Phanpy Mastodon web interface. While Phanpy offers a brilliant, minimalist way to interact with the Fediverse, it lacks a native desktop application. Relying on a pinned browser tab simply didn’t cut it for a tool I need constant, seamless access to.

I needed a dedicated window, system tray integration, and a persistent background process — essentially, a first-class desktop citizen. This led me to Nativefier.

Nativefier is a powerful command-line tool that takes any web URL and neatly wraps it into a native OS executable using the Electron framework. It essentially creates a standalone, customizable browser environment dedicated entirely to your specific web app. You get your own icon, isolated storage, and OS-level window management without having to write a full desktop application from scratch.

While the solution I am sharing here was born out of my need for a Zocial Linux app, the underlying architecture is highly versatile. You can apply this exact approach to wrap almost any web application (like a self-hosted Matrix client or a project management dashboard). Furthermore, while this guide utilizes a self-hosted Gitea instance for the CI/CD pipeline, the logic translates perfectly to GitHub Actions or GitLab CI.

However, for the scope of this article, we will focus on building a streamlined, automated deployment pipeline using Gitea to deliver a perfect background-first application for the Linux desktop.

Prepare your Gitea CI/CD Pipeline

One of the main goals of this project is to keep your local machine clean. Instead of installing Node.js, npm, and Nativefier locally just to build an app once in a while, we can offload this task to our CI/CD pipeline. Every time you want to update the app or tweak a setting, Gitea will automatically build a fresh, ready-to-use artifact.

Prerequisites

Before writing the pipeline, ensure your repository has a few basic assets in its root directory:

  • icon.png: A high-resolution image (at least 512×512) that Nativefier will use for the application icon, the system tray, and the desktop starter.
  • install-zocial.sh: Our custom bash script to handle the deployment (we will cover this in the next section).
  • The Workflow Directory: Create the folder structure “.gitea/workflows/” to house the pipeline configuration.

The Build Workflow (build.yaml)

Below is the YAML configuration that tells the Gitea Runner exactly how to construct the app.

name: Build Zocial Electron App

on:
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Setup Environment
        run: apk add --no-cache git bash
        shell: sh

      - name: Checkout Code
        uses: actions/checkout@v3
        with:
          fetch-depth: 1

      - name: Install Nativefier
        run: npm install -g nativefier

      - name: Build and Package
        run: |
          # 1. Preparation
          rm -rf ./dist
          TARGET="./dist/Zocial-linux-x64"
          
          # 2. Nativefier Build (cleanly formatted)
          nativefier --name "Zocial" \
            --app-id "zocial" \
            --app-copyright "Dome" \
            --platform linux \
            --arch x64 \
            --icon ./icon.png \
            --tray "start-in-tray" \
            --counter \
            --single-instance \
            --internal-urls ".*zocial\.ztfr\.eu.*" \
            "https://zocial.ztfr.eu" ./dist
          
          # 3. Collect files for the installer
          cp ./install-zocial.sh "$TARGET/"
          cp ./icon.png "$TARGET/"
          
          # 4. Generate Desktop Entry (stable via echo)
          echo "[Desktop Entry]" > "$TARGET/zocial.desktop"
          echo "Name=Zocial" >> "$TARGET/zocial.desktop"
          echo "Comment=Zocial Web App" >> "$TARGET/zocial.desktop"
          echo "Exec=/opt/zocial/Zocial --tray --hidden" >> "$TARGET/zocial.desktop"
          echo "Icon=/opt/zocial/icon.png" >> "$TARGET/zocial.desktop"
          echo "Type=Application" >> "$TARGET/zocial.desktop"
          echo "Categories=Network;WebBrowser;" >> "$TARGET/zocial.desktop"
          echo "Terminal=false" >> "$TARGET/zocial.desktop"
          echo "StartupWMClass=zocial" >> "$TARGET/zocial.desktop"
          
          # 5. Finalize permissions
          chmod -R 755 "$TARGET/"
          chmod +x "$TARGET/install-zocial.sh"
          chmod +x "$TARGET/Zocial"
        shell: sh

      - name: Upload Artifact
        uses: actions/upload-artifact@v3
        with:
          name: zocial-linux-x64
          path: ./dist/Zocial-linux-x64/

Let us break down what happens under the hood. The pipeline spins up a Node environment, checks out the repository, and globally installs Nativefier via npm. The real magic, however, happens within the nativefier command execution. We pass several crucial flags to shape the behavior of our wrapper:

  • –single-instance: This is essential for a true desktop application. It ensures that clicking the app icon again will bring the existing window to the front, rather than spawning a duplicate, resource-heavy process.
  • -counter: A vital feature for communication tools. If the web app updates its title with an unread count (e.g., (2) Notifications), Nativefier translates this into a native notification badge directly on the system tray icon.
  • –internal-urls “.*YOUR_DOMAIN.*”: This regex filter ensures that internal links open seamlessly inside the Electron wrapper, while external links (like a shared article or video) safely open in your default system browser.
  • –tray “start-in-tray”: This is the secret sauce for background apps. Rather than wrestling with the Linux desktop environment using hacky sleep commands or external window managers, this flag bakes the behavior directly into the application. The app checks for a system tray upon launch, creates the icon, and deliberately suppresses the initial window mapping.

Generating the Desktop Entry

Nativefier compiles the executable binary, but Linux desktop environments (like GNOME, KDE, or XFCE) rely on .desktop files to recognize applications and display them in your system menu or app grid.

Instead of writing this file manually on the target machine, our pipeline automates its creation using a series of echo commands. It dynamically generates a zocial.desktop file right inside the build artifact folder. This file defines the application name, points the Exec path to our intended “/opt/zocial/Zocial” installation directory, and sets Terminal=false so no blank command-line window opens in the background.

Instead of writing this file manually on the target machine, our pipeline automates its creation using a series of echo commands. It dynamically generates a zocial.desktop file right inside the build artifact folder. This file defines the application name, points the Exec path to our intended “/opt/zocial/Zocial” installation directory, and sets Terminal=false so no blank command-line window opens in the background.

Using Bash to Automate the Install Process

At this point, our Gitea pipeline has successfully generated a ZIP artifact containing the compiled Linux binary, the application icon, and the dynamically generated .desktop file.

However, manually extracting this archive, moving it to a system directory, setting permissions, and updating the application cache every time you want to upgrade your app is tedious. To solve this, we bundle a deployment script (install-zocial.sh) directly into the pipeline’s output.

The Installation Script

The goal of this bash script is to provide a single-command upgrade and installation process. Here is the core logic we use:

#!/bin/bash

# Configuration
APP_NAME="Zocial"
INSTALL_DIR="/opt/zocial"

# Determine the real user (even if executed with sudo)
REAL_USER=${SUDO_USER:-$USER}
REAL_HOME=$(getent passwd "$REAL_USER" | cut -d: -f6)
DESKTOP_FILE_TARGET="$REAL_HOME/.local/share/applications/zocial.desktop"
AUTOSTART_DIR="$REAL_HOME/.config/autostart"

echo "🚀 Starting $APP_NAME installation/update for user: $REAL_USER..."

# 1. Check if we are in the correct directory
if [ ! -f "./Zocial" ]; then
    echo "❌ Error: File 'Zocial' not found. Please run this script from inside the extracted directory."
    exit 1
fi

# 2. Install to /opt (requires sudo)
if [ -d "$INSTALL_DIR" ]; then
    echo "🔄 Cleaning up $INSTALL_DIR..."
    sudo rm -rf "$INSTALL_DIR"/*
else
    echo "📂 Creating $INSTALL_DIR..."
    sudo mkdir -p "$INSTALL_DIR"
fi

echo "📦 Copying files to $INSTALL_DIR..."
sudo cp -r * "$INSTALL_DIR/"

# 3. Set permissions
echo "🔐 Setting permissions..."
sudo chmod +x "$INSTALL_DIR/Zocial"

# 4. Install desktop entry for the REAL user
echo "🖥️ Installing desktop entry in $REAL_HOME..."
mkdir -p "$REAL_HOME/.local/share/applications"
cp "$INSTALL_DIR/zocial.desktop" "$DESKTOP_FILE_TARGET"
chown "$REAL_USER":"$REAL_USER" "$DESKTOP_FILE_TARGET"

# 5. Setup autostart (for the REAL user)
echo "⚙️ Setting up autostart for $REAL_USER..."
mkdir -p "$AUTOSTART_DIR"
cp "$DESKTOP_FILE_TARGET" "$AUTOSTART_DIR/"
chown "$REAL_USER":"$REAL_USER" "$AUTOSTART_DIR/zocial.desktop"

# 6. Update desktop database as the real user
sudo -u "$REAL_USER" update-desktop-database "$REAL_HOME/.local/share/applications/"

# 7. Clean application cache for the real user
echo "🧹 Cleaning application cache for $REAL_USER..."
sudo -u "$REAL_USER" rm -rf "$REAL_HOME/.cache/thumbnails/*"
sudo -u "$REAL_USER" rm -rf "$REAL_HOME/.config/zocial-nativefier-*"

echo "✅ Done! You can now launch $APP_NAME from your application menu (it will also start automatically in the tray)."

Overcoming the “Sudo Trap”

If you look closely at the script, you’ll notice we take special care to define REAL_USER and REAL_HOME.

Because installing software to the /opt/ directory requires root privileges, the script must be executed with sudo. However, if we simply used ~ or $HOME to copy our .desktop file while running under sudo, the file would mistakenly end up in /root/.local/share/applications/—and the app would never appear in your regular user’s application menu. By extracting the original user via $SUDO_USER, we ensure the system-level files go to root, while the UI-level files go exactly where your desktop environment expects them.

The Autostart Integration

The final piece of the puzzle is the autostart behavior. Linux desktop environments (like GNOME) automatically scan the ~/.config/autostart/ directory upon login and execute any .desktop files they find.

Because we already baked the –tray “start-in-tray” flag into the binary during the CI/CD build phase, we simply need to copy our standard .desktop entry into this autostart folder.

When you boot up your computer:

  1. The system reads the autostart entry.
  2. The Zocial binary executes.
  3. The app recognizes the “start-in-tray” directive, meaning it skips drawing the main window entirely.
  4. Zocial silently settles into your system tray, ready to send notifications and waiting for you to click it.

If you ever prefer to start the application manually and want to disable the autostart feature, you can either manage it via your OS “Startup Applications” GUI or simply remove step 4 from the bash script before running it.

Conclusion

We started this project with a common frustration: essential web applications getting lost in a sea of browser tabs. By combining the power of Nativefier with a Gitea CI/CD pipeline, we transformed a simple web interface into a first-class Linux desktop citizen.

Let us recap what we have achieved with this setup:

  • Native Desktop Feel: Zocial now runs in its own isolated Electron environment, complete with a dedicated window, an application menu entry, and notification badges.
  • Background-First Design: Thanks to the –tray “start-in-tray” flag and the autostart integration, the app lives quietly in the background, ready the moment you log in, without cluttering your workspace.
  • A Clean Local Machine: Because the heavy lifting is handled by the Gitea Runner, your local OS remains free of Node.js dependencies, npm packages, and build clutter.
  • One-Click Updates: Whenever Nativefier receives an update, or you want to tweak a configuration, a single click in Gitea generates a fresh, deployable .zip artifact.

The true beauty of this workflow, however, is its versatility. While this guide focused on wrapping my Phanpy fork, the repository we built is essentially a universal blueprint. You can take this exact Gitea Action and bash script, swap out the URL and the icon, and instantly create desktop wrappers for a self-hosted Matrix client, a project management dashboard, or any web tool you rely on daily. By leveraging automation, we took control of how we interact with the web on our own terms.

So if you want to explore the complete codebase, review the Gitea Actions workflow, or use the installer script as a foundation for your own projects, you can check out the real-life repository here.

I hope this guide helps you tame your browser tabs and brings a bit more focus to your Linux desktop. Happy self-hosting and building!

Leave a Reply

Your email address will not be published. Required fields are marked *