<?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: Zerolock</title>
    <description>The latest articles on DEV Community by Zerolock (zerolock).</description>
    <link>https://dev.to/zerolock</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.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F14613%2Fac5c9313-929b-4807-b6ed-3e2a26418ffb.jpg</url>
      <title>DEV Community: Zerolock</title>
      <link>https://dev.to/zerolock</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zerolock"/>
    <language>en</language>
    <item>
      <title>How I replaced WhatsApp and phone calls at a construction company using Mattermost</title>
      <dc:creator>Mišo Oroz</dc:creator>
      <pubDate>Tue, 27 Oct 2026 09:34:29 +0000</pubDate>
      <link>https://dev.to/zerolock/how-i-replaced-whatsapp-and-phone-calls-at-a-construction-company-using-mattermost-4gbf</link>
      <guid>https://dev.to/zerolock/how-i-replaced-whatsapp-and-phone-calls-at-a-construction-company-using-mattermost-4gbf</guid>
      <description>&lt;p&gt;At a construction company where I worked as IT manager, communication was fragmented across three channels that never talked to each other.&lt;/p&gt;

&lt;p&gt;Seven people in the office. Around twenty workers and laborers across active construction sites, four or five on any given site at a time. Messages on personal WhatsApp numbers. Phone calls. The occasional email nobody read in time. Site photos sent to random personal chats. The office never knew what was happening on site unless someone picked up the phone.&lt;/p&gt;

&lt;p&gt;The standard answer would have been Slack or Microsoft Teams. I went with Mattermost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Mattermost
&lt;/h2&gt;

&lt;p&gt;Three reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-hosted.&lt;/strong&gt; The company's data, site photos, project updates, internal decisions, stays on a server I control. Not on Slack's infrastructure, not on Microsoft's. For a small business that was a deliberate choice, not a compliance checkbox.&lt;/p&gt;

&lt;p&gt;One nuance worth knowing: push notifications for the official mobile apps are relayed through Mattermost's hosted push service, which forwards them to Apple and Google. The message content stays on my server, the notification metadata does not. Fully self-hosted push means compiling your own mobile apps with your own push proxy, which was more complexity than a company of this size needed. For this deployment, the trade-off was acceptable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mobile app works.&lt;/strong&gt; Construction workers are not going to open a browser on their phone. The Mattermost Android app is fast and sends push notifications reliably. That was non-negotiable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The desktop client behaves well on KDE Plasma.&lt;/strong&gt; Tray icon works, notifications integrate cleanly, no visual glitches. Relevant because KDE is the Linux desktop I deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deployment
&lt;/h2&gt;

&lt;p&gt;I installed Mattermost on an existing VPS that already hosted other internal services, with PostgreSQL as the database and Nginx in front for TLS. The official docs cover this well. What mattered more was what came after the install.&lt;/p&gt;

&lt;p&gt;I created the team and built a minimal channel structure: one general channel, one per active project, one for the office, one for site updates.&lt;/p&gt;

&lt;p&gt;I kept it flat on purpose. Over-organizing before people start using the tool is a waste of time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Training the office and the site supervisors
&lt;/h2&gt;

&lt;p&gt;The office staff took about thirty minutes each. Channels instead of group chats clicked quickly once I showed one real example.&lt;/p&gt;

&lt;p&gt;The workers took longer, not because the app was difficult, but because structured communication was new to them. What worked was framing it simply: the work WhatsApp, but owned by the company, and nothing gets lost.&lt;/p&gt;

&lt;p&gt;I ran one group session on site with the supervisors' phones. Showed them how to post a photo to the right project channel. They then handled it with their own crews. That was most of the training.&lt;/p&gt;

&lt;p&gt;Then I left them alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changed
&lt;/h2&gt;

&lt;p&gt;Before Mattermost, the only way to know what was happening on site was a phone call. The office would ask, someone would answer, information would get lost in translation.&lt;/p&gt;

&lt;p&gt;After the deployment, the geometra (site surveyor) could open the project channel and see what the capocantiere (site foreman) had posted an hour ago. The owner could check site progress without calling anyone. The office had context when clients called with questions. Everyone was looking at the same information at the same time.&lt;/p&gt;

&lt;p&gt;Nobody asked for this explicitly. It emerged from having one place where site updates actually landed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened after I stopped managing it
&lt;/h2&gt;

&lt;p&gt;A few weeks in I checked the channels. The team had reorganized things themselves. Created channels I hadn't set up. Developed their own conventions. One site supervisor started pinning important updates so the office could find them later.&lt;/p&gt;

&lt;p&gt;I had not asked for any of that.&lt;/p&gt;

&lt;p&gt;If the tool is solid and the initial training is honest, people adapt it to their actual workflow without hand-holding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things I'd tell anyone deploying this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Keep the initial channel structure minimal.&lt;/strong&gt; Three or four channels maximum at the start. Let the team build it out once they understand how they actually communicate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plan for storage before you go live.&lt;/strong&gt; Construction sites generate a lot of photos. Twenty workers with phones and something visual about their work will fill a disk faster than you expect. Size the VPS accordingly, set file size limits from day one, and decide early what happens to old project channels: archive them, keep the files, and make sure your backup covers both the database and the file store.&lt;/p&gt;

&lt;p&gt;I no longer work at that company. The setup stayed, which is the part I care about. I carry the same approach into my current work at Zerolock, where Mattermost is part of the standard stack I deploy for clients who need internal communication without handing data to third-party platforms.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://zerolock.it" rel="noopener noreferrer"&gt;zerolock.it&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mattermost</category>
      <category>selfhosted</category>
      <category>opensource</category>
      <category>linux</category>
    </item>
    <item>
      <title>KDE Plasma crashes after system update on Kubuntu 26.04: fontconfig race condition fix</title>
      <dc:creator>Mišo Oroz</dc:creator>
      <pubDate>Wed, 23 Sep 2026 14:35:37 +0000</pubDate>
      <link>https://dev.to/zerolock/kde-plasma-crashes-after-system-update-on-kubuntu-2604-fontconfig-race-condition-fix-2ebm</link>
      <guid>https://dev.to/zerolock/kde-plasma-crashes-after-system-update-on-kubuntu-2604-fontconfig-race-condition-fix-2ebm</guid>
      <description>&lt;p&gt;Last week I updated a Kubuntu 26.04 LTS workstation and came back to a broken desktop. KDE Plasma would start, the desktop would flash for a second, then disappear. plasmashell kept crashing in a loop. The machine was otherwise fine: Konsole worked, Firefox worked, Thunderbird worked, the system was responsive. Just no desktop.&lt;/p&gt;

&lt;p&gt;This is what I found and how I fixed it permanently.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was happening
&lt;/h2&gt;

&lt;p&gt;The crash was a SIGSEGV in plasmashell, repeating every few seconds with &lt;code&gt;Failed with result 'core-dump'&lt;/code&gt; in the service logs. After digging into the stack trace, the culprit was fontconfig.&lt;/p&gt;

&lt;p&gt;The root cause is a race condition at login. When you log in after a system update that touches font packages, plasmashell starts reading the font cache at the same time something else triggers its regeneration. fontconfig ends up writing a new cache while plasmashell is already reading the old one, which causes a segfault inside &lt;code&gt;FcCharSetHasChar&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It is a timing issue, which is why it does not happen on every update and why a simple restart sometimes appears to fix it temporarily.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnosing
&lt;/h2&gt;

&lt;p&gt;Before touching anything, confirm the crash is what I described:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;coredumpctl list &lt;span class="nt"&gt;--since&lt;/span&gt; today &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see plasmashell crashes. Then read the stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;coredumpctl info plasmashell &lt;span class="nt"&gt;--no-pager&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A40&lt;/span&gt; &lt;span class="s2"&gt;"Stack trace of thread"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for &lt;code&gt;FcCharSetHasChar&lt;/code&gt; in the trace. If it is there, you have the same issue.&lt;/p&gt;

&lt;p&gt;Also worth checking the session log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; warning | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s2"&gt;"plasmashell|fontconfig"&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Temporary fix
&lt;/h2&gt;

&lt;p&gt;This gets the desktop back immediately without a reboot:&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;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; ~/.cache/fontconfig
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/cache/fontconfig/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;fc-cache &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt;
fc-cache &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt;
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; reset-failed plasma-plasmashell
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; restart plasma-plasmashell
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The desktop should come back within a few seconds. This works until the next reboot, at which point the race condition can happen again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permanent fix
&lt;/h2&gt;

&lt;p&gt;The real solution is to force a font cache regeneration before plasmashell starts. You do this with a systemd drop-in that adds an ExecStartPre directive to the plasmashell 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;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.config/systemd/user/plasma-plasmashell.service.d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create the drop-in file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/.config/systemd/user/plasma-plasmashell.service.d/fontcache.conf &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[Service]
ExecStartPre=/usr/bin/fc-cache -f
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload systemd and reboot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After login, verify it worked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; status plasma-plasmashell | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You want to see this in the output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="nt"&gt;Process: ExecStartPre&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;/usr/bin/fc-cache -f (code=exited, status=0/SUCCESS)
&lt;span class="err"&gt;Active:&lt;/span&gt; &lt;span class="err"&gt;active&lt;/span&gt; &lt;span class="err"&gt;(running)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see that, the drop-in is working and the font cache is being regenerated before plasmashell touches it on every boot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this works
&lt;/h2&gt;

&lt;p&gt;The systemd drop-in ensures fc-cache completes before plasmashell starts. No race, no partial file reads, no segfault. It adds a second or two to login time, which is a reasonable trade for a stable desktop.&lt;/p&gt;

&lt;p&gt;The drop-in survives updates because it lives in your user config directory, not in the system service file. If a future KDE or fontconfig update changes the timing behavior, you can simply remove the drop-in and check if the issue is gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment
&lt;/h2&gt;

&lt;p&gt;Kubuntu 26.04 LTS, KDE Plasma 6.6.6, kernel 7.0.0-34, Intel Raptor Lake i915. I deploy Kubuntu 26.04 on client workstations and hit this after a routine update. The fix has held across multiple reboots and subsequent updates.&lt;/p&gt;

&lt;p&gt;If you are seeing something similar but the stack trace does not mention fontconfig, this drop-in will not help. The crash is something else. Check the coredumpctl output carefully before applying anything.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://zerolock.it" rel="noopener noreferrer"&gt;zerolock.it&lt;/a&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>kde</category>
      <category>kubuntu</category>
      <category>debugging</category>
    </item>
    <item>
      <title>WireGuard VPN for Production: The Exact Setup I Deploy for Small Businesses</title>
      <dc:creator>Mišo Oroz</dc:creator>
      <pubDate>Tue, 15 Sep 2026 09:11:45 +0000</pubDate>
      <link>https://dev.to/zerolock/wireguard-vpn-for-production-the-exact-setup-i-deploy-for-small-businesses-3m41</link>
      <guid>https://dev.to/zerolock/wireguard-vpn-for-production-the-exact-setup-i-deploy-for-small-businesses-3m41</guid>
      <description>&lt;p&gt;WireGuard is the VPN I deploy for every small business I migrate to Linux. Not because it is trendy, but because it is the most honest piece of networking software I have worked with in fifteen years.&lt;/p&gt;

&lt;p&gt;In 2026, WireGuard is the default VPN protocol for a reason: it is in the mainline Linux kernel since version 5.6, independently audited, and faster than OpenVPN on the same hardware. For small businesses this means no kernel modules to maintain, no CVE backlog, and no performance complaints.&lt;/p&gt;

&lt;p&gt;The entire trust model fits in a single config file. When something breaks, you can audit it in minutes. That matters at 8am on a Monday when a client cannot connect and you need to diagnose fast.&lt;/p&gt;

&lt;p&gt;This is not a theoretical walkthrough. This is exactly what I deploy in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Server with static public IP&lt;/li&gt;
&lt;li&gt;UDP 51820 open on server firewall&lt;/li&gt;
&lt;li&gt;SSH access to the server&lt;/li&gt;
&lt;li&gt;Client machine with working internet connection&lt;/li&gt;
&lt;li&gt;A clear decision on whether all traffic routes through the tunnel or only the internal network (more on this below)&lt;/li&gt;
&lt;li&gt;A backup access plan if the tunnel breaks (physical access, out-of-band SSH, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why WireGuard specifically
&lt;/h2&gt;

&lt;p&gt;The WireGuard codebase is roughly 4,000 lines. OpenVPN's core daemon is closer to 70,000. This is not a marketing detail. Smaller codebase means smaller attack surface, faster patching, and a security posture you can actually reason about.&lt;/p&gt;

&lt;p&gt;WireGuard has been independently audited. The Cure53 audit in 2021 and the Trail of Bits review found no critical vulnerabilities. OpenVPN has also been audited, but its larger codebase makes comprehensive auditing more time-consuming and leaves more surface area for potential issues.&lt;/p&gt;

&lt;p&gt;I evaluated three options before standardizing on WireGuard:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenVPN:&lt;/strong&gt; mature, flexible, but complex. Requires certificate management, has a larger attack surface, and is noticeably slower on low-end hardware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IPsec/IKEv2:&lt;/strong&gt; fast and secure, but configuration is opaque. Debugging failures often requires deep protocol knowledge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WireGuard:&lt;/strong&gt; minimal, audited, fast. The trade-off is manual key management, which is acceptable for teams under 20 people.&lt;/p&gt;

&lt;p&gt;For small businesses that cannot afford enterprise security teams, choosing audited software with a minimal codebase is one of the few concrete security decisions they can make.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;p&gt;A Linux server you control. In my case a VPS hosted in Italy, running Debian 12. A static IP on the server side. A client machine running Linux, in most cases Kubuntu 26.04 LTS.&lt;/p&gt;

&lt;p&gt;I use a VPS for the relay in most deployments because it gives clients a stable public IP without managing physical hardware at the office. For businesses with an on-premise server already in place, running WireGuard directly on that machine works equally well. The config is identical.&lt;/p&gt;

&lt;p&gt;Debian 12 is the default for server deployments: predictable release cycle, five years of security updates, no surprises. I avoid rolling releases on anything client-facing.&lt;/p&gt;

&lt;p&gt;WireGuard uses public key cryptography. Each peer generates a key pair. You exchange public keys. No certificates, no certificate authorities, no complexity that lives outside the config file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server setup
&lt;/h2&gt;

&lt;p&gt;Install WireGuard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;wireguard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate the server key pair. Set permissions before the key is used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wg genkey &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/wireguard/privatekey
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /etc/wireguard/privatekey
wg pubkey &amp;lt; /etc/wireguard/privatekey &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/wireguard/publickey
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the server configuration at &lt;code&gt;/etc/wireguard/wg0.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Interface]&lt;/span&gt;
&lt;span class="py"&gt;PrivateKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;server_private_key&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;Address&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10.0.0.1/24&lt;/span&gt;
&lt;span class="py"&gt;ListenPort&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;51820&lt;/span&gt;
&lt;span class="py"&gt;PostUp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o &amp;lt;interface&amp;gt; -j MASQUERADE&lt;/span&gt;
&lt;span class="py"&gt;PostDown&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o &amp;lt;interface&amp;gt; -j MASQUERADE&lt;/span&gt;

&lt;span class="nn"&gt;[Peer]&lt;/span&gt;
&lt;span class="py"&gt;PublicKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;client_public_key&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;AllowedIPs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10.0.0.2/32&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Replace &lt;code&gt;&amp;lt;interface&amp;gt;&lt;/code&gt; with your actual network interface name.&lt;/strong&gt; On Debian 12 modern installs this is typically &lt;code&gt;ens3&lt;/code&gt;, &lt;code&gt;enp1s0&lt;/code&gt;, or similar. Check with &lt;code&gt;ip link&lt;/code&gt; before editing the config. Almost never &lt;code&gt;eth0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why 10.0.0.0/24?&lt;/strong&gt; This is RFC 1918 private space, unlikely to conflict with most office networks. I use 10.0.0.1 for the server and increment client IPs from 10.0.0.2. If your office already uses 10.0.0.0/24, choose a different range such as 10.100.0.0/24 to avoid routing conflicts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On the &lt;code&gt;/32&lt;/code&gt; for each peer:&lt;/strong&gt; this tells WireGuard that this specific public key is only allowed to use the IP 10.0.0.2. Each peer must have its own &lt;code&gt;/32&lt;/code&gt; entry. Without it, a compromised client could potentially spoof other addresses on the tunnel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note on cloud providers:&lt;/strong&gt; if you use a VPS with a cloud security group (Hetzner, DigitalOcean, AWS), open UDP 51820 in the security group rather than via iptables. The PostUp rules above handle forwarding and NAT, not port access itself.&lt;/p&gt;

&lt;p&gt;If your server has IPv6 enabled, which is common on modern VPS, add ip6tables rules as well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;PostUp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o &amp;lt;interface&amp;gt; -j MASQUERADE; ip6tables -A FORWARD -i wg0 -j ACCEPT; ip6tables -A FORWARD -o wg0 -j ACCEPT; ip6tables -t nat -A POSTROUTING -o &amp;lt;interface&amp;gt; -j MASQUERADE&lt;/span&gt;
&lt;span class="py"&gt;PostDown&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o &amp;lt;interface&amp;gt; -j MASQUERADE; ip6tables -D FORWARD -i wg0 -j ACCEPT; ip6tables -D FORWARD -o wg0 -j ACCEPT; ip6tables -t nat -D POSTROUTING -o &amp;lt;interface&amp;gt; -j MASQUERADE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable IP forwarding:&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;echo&lt;/span&gt; &lt;span class="s1"&gt;'net.ipv4.ip_forward = 1'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /etc/sysctl.conf
sysctl &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable and start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; wg-quick@wg0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Client setup
&lt;/h2&gt;

&lt;p&gt;Install WireGuard and openresolv:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;wireguard openresolv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate client key pair:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wg genkey &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/wireguard/privatekey
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /etc/wireguard/privatekey
wg pubkey &amp;lt; /etc/wireguard/privatekey &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/wireguard/publickey
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;/etc/wireguard/wg0.conf&lt;/code&gt; on the client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Interface]&lt;/span&gt;
&lt;span class="py"&gt;PrivateKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;client_private_key&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;Address&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10.0.0.2/32&lt;/span&gt;
&lt;span class="py"&gt;DNS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1.1.1.1&lt;/span&gt;

&lt;span class="nn"&gt;[Peer]&lt;/span&gt;
&lt;span class="py"&gt;PublicKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;server_public_key&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;Endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;server_public_ip&amp;gt;:51820&lt;/span&gt;
&lt;span class="py"&gt;AllowedIPs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0/0, ::/0&lt;/span&gt;
&lt;span class="py"&gt;PersistentKeepalive&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;25&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;/32&lt;/code&gt; on the client?&lt;/strong&gt; The client is a single host, not a subnet. Using &lt;code&gt;/32&lt;/code&gt; is semantically correct and avoids confusion when adding more clients later. The &lt;code&gt;/24&lt;/code&gt; on the server interface is correct because it represents the entire tunnel network.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;0.0.0.0/0, ::/0&lt;/code&gt; routes all traffic through the VPN, including IPv6. If you only need access to the internal network without routing everything, use the specific subnet instead, for example &lt;code&gt;10.0.0.0/24&lt;/code&gt;. Understand what you are routing before you route it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PersistentKeepalive = 25&lt;/code&gt; sends a keepalive every 25 seconds. Required when the client is behind NAT, which is almost always the case for small business remote workers. Without it the NAT mapping expires and the tunnel silently drops.&lt;/p&gt;

&lt;p&gt;Enable and start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; wg-quick@wg0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the tunnel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wg show
ping 10.0.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;wg show&lt;/code&gt; confirms the handshake happened. &lt;code&gt;ping 10.0.0.1&lt;/code&gt; confirms the tunnel carries traffic. Both should work before you call it done.&lt;/p&gt;

&lt;h2&gt;
  
  
  DNS on Kubuntu 26.04 with systemd-resolved
&lt;/h2&gt;

&lt;p&gt;Kubuntu 26.04 uses systemd-resolved by default. The &lt;code&gt;DNS =&lt;/code&gt; directive in wg0.conf works correctly with systemd-resolved, but you must ensure &lt;code&gt;/etc/resolv.conf&lt;/code&gt; points to the stub resolver:&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;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /etc/resolv.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;/&lt;span class="n"&gt;etc&lt;/span&gt;/&lt;span class="n"&gt;resolv&lt;/span&gt;.&lt;span class="n"&gt;conf&lt;/span&gt; -&amp;gt; ../&lt;span class="n"&gt;run&lt;/span&gt;/&lt;span class="n"&gt;systemd&lt;/span&gt;/&lt;span class="n"&gt;resolve&lt;/span&gt;/&lt;span class="n"&gt;stub&lt;/span&gt;-&lt;span class="n"&gt;resolv&lt;/span&gt;.&lt;span class="n"&gt;conf&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it points elsewhere, fix 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;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-sf&lt;/span&gt; /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bring up the tunnel and verify DNS is routing correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;resolvectl status wg0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see your configured DNS listed under the wg0 interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify no DNS leaks
&lt;/h2&gt;

&lt;p&gt;A DNS leak means your DNS queries are going outside the tunnel, exposing which domains you visit even if the traffic itself is encrypted. After bringing up the tunnel, verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short myip.opendns.com @resolver1.opendns.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should return your VPN server's public IP, not your client's real IP. If it returns your real IP, DNS is leaking.&lt;/p&gt;

&lt;p&gt;Also check for IPv6 leaks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-6&lt;/span&gt; ifconfig.co
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this returns an IP, IPv6 traffic is not going through the tunnel. Either disable IPv6 on the client or ensure &lt;code&gt;::/0&lt;/code&gt; is in &lt;code&gt;AllowedIPs&lt;/code&gt; and add an IPv6 DNS server: &lt;code&gt;DNS = 1.1.1.1, 2606:4700:4700::1111&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key management
&lt;/h2&gt;

&lt;p&gt;WireGuard has no built-in key rotation. When an employee leaves or a device is compromised, you must manually remove the &lt;code&gt;[Peer]&lt;/code&gt; block from the server config and redistribute new keys.&lt;/p&gt;

&lt;p&gt;My workflow for small businesses: I keep a simple spreadsheet with peer name (for example "alice-laptop"), public key copied from &lt;code&gt;/etc/wireguard/publickey&lt;/code&gt;, device owner, creation date, and last rotation date. Rotate keys every 12 to 18 months as standard practice. If a device is lost, revoke immediately: remove the &lt;code&gt;[Peer]&lt;/code&gt; block, restart WireGuard, and generate new keys for the affected client.&lt;/p&gt;

&lt;p&gt;This manual process is a trade-off: you gain simplicity and auditability, but lose automated lifecycle management. For teams under 20 people it is acceptable. Beyond that, consider a WireGuard management layer like Netmaker or Headscale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring and troubleshooting
&lt;/h2&gt;

&lt;p&gt;Check handshake age — should be under two minutes on an active connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wg show | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"latest handshake"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch live traffic through the tunnel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;watch &lt;span class="nt"&gt;-n1&lt;/span&gt; &lt;span class="s1"&gt;'wg show | grep transfer'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For proactive monitoring I use a simple cron job that pings the tunnel IP every 5 minutes and logs failures:&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="k"&gt;*&lt;/span&gt;/5 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; ping &lt;span class="nt"&gt;-c1&lt;/span&gt; 10.0.0.1 &lt;span class="o"&gt;||&lt;/span&gt; logger &lt;span class="s2"&gt;"WireGuard tunnel down"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the tunnel drops:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check logs: &lt;code&gt;journalctl -u wg-quick@wg0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Verify the port is reachable: &lt;code&gt;nc -vzu &amp;lt;server_ip&amp;gt; 51820&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Confirm &lt;code&gt;PersistentKeepalive = 25&lt;/code&gt; is set on the client&lt;/li&gt;
&lt;li&gt;Restart the tunnel: &lt;code&gt;systemctl restart wg-quick@wg0&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Appendix: Kubuntu 26.04 and Wayland
&lt;/h2&gt;

&lt;p&gt;Kubuntu 26.04 uses Wayland by default. This does not affect WireGuard itself, but it breaks X11-based remote desktop tools. For full remote access setups I pair WireGuard with a self-hosted RustDesk relay, which handles Wayland correctly. On Kubuntu 24.04, which still uses X11, standard remote desktop tools work without this additional step. RustDesk with a self-hosted relay is outside the scope of this guide but worth knowing before you commit to a 26.04 deployment for remote workers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final notes
&lt;/h2&gt;

&lt;p&gt;I have been running this setup across multiple client deployments since early 2026. No security incidents, no unexplained drops. WireGuard's simplicity is its strength: when something breaks, you can read the entire config and understand it in minutes.&lt;/p&gt;

&lt;p&gt;For small businesses migrating to Linux, this is the VPN layer I standardize on. And if a client or an auditor ever asks why I chose it, I can explain every line of the config. That is not something I could say about most alternatives.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://zerolock.it" rel="noopener noreferrer"&gt;zerolock.it&lt;/a&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>opensource</category>
      <category>vpn</category>
      <category>wireguard</category>
    </item>
    <item>
      <title>Nextcloud vs Google Drive: a practical comparison for small business migrations</title>
      <dc:creator>Mišo Oroz</dc:creator>
      <pubDate>Sun, 06 Sep 2026 14:20:24 +0000</pubDate>
      <link>https://dev.to/zerolock/nextcloud-vs-google-drive-a-practical-comparison-for-small-business-migrations-3obc</link>
      <guid>https://dev.to/zerolock/nextcloud-vs-google-drive-a-practical-comparison-for-small-business-migrations-3obc</guid>
      <description>&lt;p&gt;I've been migrating small businesses in northern Italy from Google Workspace and Microsoft 365 to open source stacks for a while now. One of the first questions I always get is: "What about file storage? We use Google Drive for everything."&lt;/p&gt;

&lt;p&gt;Fair question. Let me give you an honest answer, not a sales pitch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we're actually comparing
&lt;/h2&gt;

&lt;p&gt;When I say Google Drive in a business context, I mean the full Google Workspace package: Drive, Docs, Sheets, Gmail, Meet. It's a coherent platform that works well. I'm not going to pretend otherwise.&lt;/p&gt;

&lt;p&gt;Nextcloud is a self-hosted (or managed-hosted) file sync and collaboration platform. Out of the box it handles file storage, sharing, and sync across devices. With apps you can add calendar, contacts, talk (video calls), and collaborative document editing via Nextcloud Office (built on Collabora).&lt;/p&gt;

&lt;p&gt;So the comparison isn't just "where do files live." It's about how your client's business actually runs day to day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real differences that matter
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Data location and sovereignty&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With Google Drive, your files live on Google's infrastructure. Full stop. For most small businesses this isn't a problem until it is: GDPR compliance questions, a client contract that requires data to stay in Italy, or a sector with specific regulations (legal, medical, financial).&lt;/p&gt;

&lt;p&gt;With Nextcloud you choose where data lives. I typically set up clients on Italian or Swiss-based infrastructure, choosing providers that keep data within EU jurisdiction. The client owns the data, period.&lt;/p&gt;

&lt;p&gt;This matters more than people realize. I've had clients lose access to their Google Workspace because of billing issues during a holiday weekend. With self-hosted infrastructure, you control the keys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost structure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Google Workspace Business Starter runs around 6.80 euros per user per month at the time of writing (check current pricing on their site as this changes). For a team of 8 that's around 653 euros per year, just for the base tier. Storage limits apply at 30 GB pooled per user, and many teams end up on a higher plan within a year.&lt;/p&gt;

&lt;p&gt;Nextcloud itself is open source and free. You pay for hosting. A decent VPS with enough storage for a small team costs 15 to 30 euros per month, depending on specs and provider. For 8 users that's 180 to 360 euros per year, with no per-user licensing.&lt;/p&gt;

&lt;p&gt;The math is straightforward. The savings compound over time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feature parity&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is where people get nervous. Let me be direct: Nextcloud is not Google Drive. Some things work differently, some things work better, some things are more manual.&lt;/p&gt;

&lt;p&gt;File sync across devices works well. The desktop clients for Linux, Windows, and macOS are stable. Mobile apps exist for Android and iOS.&lt;/p&gt;

&lt;p&gt;Collaborative editing works if you install Nextcloud Office. It's not as smooth as Google Docs in my experience, but it's functional for most business use cases. If your team lives inside documents all day, evaluate this carefully before committing.&lt;/p&gt;

&lt;p&gt;Search is decent but not Google-level. For a team that stores lots of documents and searches constantly, this is worth testing with real data before the migration.&lt;/p&gt;

&lt;p&gt;Sharing links, permissions, versioning: all there and well implemented.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Administration overhead&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the honest part. Google Workspace has zero admin overhead for the IT person. You click, it works, Google handles updates, security patches, and uptime.&lt;/p&gt;

&lt;p&gt;Nextcloud requires someone to handle updates, monitor the instance, and occasionally deal with issues. If that someone is you (the IT consultant), you need to factor this into your service contract. If it's supposed to be the client on their own, think carefully.&lt;/p&gt;

&lt;p&gt;For the clients I manage this is part of the monthly support I provide. It's not heavy lifting, but it's not zero either.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I recommend Nextcloud
&lt;/h2&gt;

&lt;p&gt;I push for Nextcloud when the client has 2 to 20 users with straightforward storage and sharing needs and no heavy real-time collaboration on complex documents.&lt;/p&gt;

&lt;p&gt;There's a compliance or sovereignty angle that matters. Legal practices, small financial advisors, healthcare adjacent businesses. Anything where "your data is on Google's servers" is a problem worth solving.&lt;/p&gt;

&lt;p&gt;The client wants predictable costs with no per-user licensing that grows as they hire.&lt;/p&gt;

&lt;p&gt;They're already moving to a Linux-based workstation setup, so the open source philosophy is consistent across the stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I don't
&lt;/h2&gt;

&lt;p&gt;If the client's workflow is deeply integrated into Google Workspace, a migration is painful and the value is lower. At Zerolock we've walked away from a couple of these rather than force a migration that would hurt the client more than help them. Heavy Docs and Sheets collaboration, Gmail filtering built into daily operations, Google Meet for all external calls: these are signals to evaluate carefully before committing to a switch.&lt;/p&gt;

&lt;p&gt;Same with shops that use Teams heavily for video and internal communication. The Nextcloud Talk feature is improving but it's not Teams, and if video collaboration is critical, that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The migration process in practice
&lt;/h2&gt;

&lt;p&gt;For a typical 5 to 8 person team, here's roughly how it goes.&lt;/p&gt;

&lt;p&gt;Set up the Nextcloud instance. I use Docker on a VPS, behind a reverse proxy, with automated backups to a separate location.&lt;/p&gt;

&lt;p&gt;Migrate existing files from Google Drive using rclone. It handles the OAuth flow cleanly and can do incremental syncs during the transition period.&lt;/p&gt;

&lt;p&gt;Install and configure desktop clients on each workstation. On Linux (Kubuntu in my case) this is straightforward via the official package.&lt;/p&gt;

&lt;p&gt;Train the team. Usually half a day. Most people adapt quickly because the core experience, folder, file, share, is familiar.&lt;/p&gt;

&lt;p&gt;Run parallel for two to four weeks. Both systems active, with the team gradually moving their work to Nextcloud. Then cut over.&lt;/p&gt;

&lt;p&gt;The hardest part is usually email. If they're leaving Gmail too, that's a separate migration with Thunderbird or a webmail setup. But that's a different article.&lt;/p&gt;

&lt;h2&gt;
  
  
  One thing I'd add
&lt;/h2&gt;

&lt;p&gt;I've found that clients are more comfortable with Nextcloud than they expect, mostly because it looks and behaves like a normal file system. The "everything is a proprietary document format" model confuses some users who just want to store a PDF and find it later. Nextcloud handles that interaction more naturally.&lt;/p&gt;

&lt;p&gt;It's not the right tool for every situation. But for a lot of small businesses that don't need the full cloud suite and do care about where their data lives, it's a solid, honest choice.&lt;/p&gt;

&lt;p&gt;If you're doing similar migrations and have questions about the setup, happy to compare notes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://zerolock.it" rel="noopener noreferrer"&gt;zerolock.it&lt;/a&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>nextcloud</category>
      <category>googledrive</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How we migrated an Italian logistics company from Windows to Linux: a real case study</title>
      <dc:creator>Mišo Oroz</dc:creator>
      <pubDate>Wed, 02 Sep 2026 14:34:47 +0000</pubDate>
      <link>https://dev.to/zerolock/how-we-migrated-an-italian-logistics-company-from-windows-to-linux-a-real-case-study-3gpc</link>
      <guid>https://dev.to/zerolock/how-we-migrated-an-italian-logistics-company-from-windows-to-linux-a-real-case-study-3gpc</guid>
      <description>&lt;p&gt;In early 2026 I migrated a logistics and transport company in Bergamo, Italy from a fully proprietary setup to Linux and open source. Five workstations, five employees, zero technical background on their side. Here is what we did, what happened, and what I learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  The starting point
&lt;/h2&gt;

&lt;p&gt;The company was running Windows on every workstation, Microsoft Office for documents, a third-party antivirus renewed annually, and an external IT technician called in every time something broke. They had no internal IT knowledge and no interest in acquiring any. They just wanted things to work.&lt;/p&gt;

&lt;p&gt;Their main pain points were slow machines, frequent crashes after Windows updates, and a recurring bill that kept growing without any visible improvement.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we installed
&lt;/h2&gt;

&lt;p&gt;I replaced everything with a fully open source stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kubuntu as the operating system on all five workstations&lt;/li&gt;
&lt;li&gt;LibreOffice in place of Microsoft Office&lt;/li&gt;
&lt;li&gt;Thunderbird for email&lt;/li&gt;
&lt;li&gt;Nextcloud for file sharing and internal document management, self-hosted on a server in Italy&lt;/li&gt;
&lt;li&gt;Mattermost for internal team communication&lt;/li&gt;
&lt;li&gt;pfSense as the network firewall&lt;/li&gt;
&lt;li&gt;WireGuard for VPN access&lt;/li&gt;
&lt;li&gt;Automated encrypted backups running nightly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total recurring licence cost after migration: zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  The migration process
&lt;/h2&gt;

&lt;p&gt;The full migration took approximately one week. I configured everything on a test machine first, validated all workflows with the team, then rolled out workstation by workstation during off-hours to avoid interruptions.&lt;/p&gt;

&lt;p&gt;The biggest challenge was not technical. It was psychological. People are used to what they know. I spent as much time sitting with each employee and showing them that LibreOffice opens their Word files and Nextcloud works like Google Drive as I did on the actual technical setup.&lt;/p&gt;

&lt;p&gt;After three days they stopped asking about Windows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results after six months
&lt;/h2&gt;

&lt;p&gt;Six months in, the company has not called me for a single ordinary technical issue. The machines are faster. Updates do not break anything. There have been zero security incidents.&lt;/p&gt;

&lt;p&gt;The most concrete result: each employee recovered roughly one hour per day that was previously lost to slowdowns, crashes and waiting for the external technician. On five people that is five hours a day returned to productive work.&lt;/p&gt;

&lt;p&gt;Licence costs are gone. The infrastructure belongs to them, not to a vendor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters beyond one company
&lt;/h2&gt;

&lt;p&gt;Most Italian SMEs are in the same situation. They pay recurring fees for software they do not own, store data on foreign servers subject to the US Cloud Act, and depend on vendors who can change the rules at any time.&lt;/p&gt;

&lt;p&gt;Linux and open source infrastructure is not experimental. It is mature, stable, and ready for business use. The barrier is not technical. It is awareness.&lt;/p&gt;

&lt;p&gt;I wrote the Zerolock Manifesto (&lt;a href="https://zerolock.org" rel="noopener noreferrer"&gt;https://zerolock.org&lt;/a&gt;) on exactly this topic. If you are working on similar migrations or have questions about the stack, I am happy to discuss in the comments.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>opensource</category>
      <category>selfhosted</category>
      <category>privacy</category>
    </item>
  </channel>
</rss>
