These are my notes on setting and maintaining a desktop/workstation system, a successor to the older CentOS 7 workstation, to be used--among other things--with the private server setup and simpler server setup.
      My goals were a working setup, along with an old system, simple
      and close to the standard one, and with
      encrypted /home (see also: personal data
      storage). To avoid possible confusion during installation or
      when some repairs are needed, I keep a sheet of paper with
      partitions listed on it.
    
      I went for Unofficial non-free images including firmware
      packages, since I need GNU documentation and the Nvidia
      proprietary driver anyway (unnecessary as of Debian 12, since
      proprietary firmware is included into official images, and that
      Nvidia card is not supported anymore), and it is more suitable
      for a rescue USB stick. Picked a live Xfce image, to be able to
      poke it briefly (and ensure that it works fine with the
      hardware) before installation, as well as for possible later use
      as a rescue system. Though live images come with a drawback of
      installing live-task-* packages, including
      localization ones for all the supported languages, so you end up
      with hundreds of additional and unused packages to upgrade
      regularly; netinst produces a cleaner system, but
      they can also be removed manually afterwards. Xfce is not as
      bloated and broken as GNOME and KDE, but not as half-baked and
      broken as most of the others. Apparently MATE and Cinnamon aim a
      similar level of complexity, and I hear good things about those,
      too. I downloaded the image via BitTorrent, and as
      the Installation Guide suggests, did the equivalent of cp
      debian.iso /dev/sdX && sync.
    
There is a graphical installer available from the live system itself, which is handy for looking up documentation on the web while installing, but its functionality differs from that of the regular installer: there is no option to make an EFI system partition (ESP) explicitly, so I rebooted and used the regular installer. Although while installing Debian on another machine a bit later, I noticed that it would handle fine a FAT32 partition mounted into /boot/efi, without requiring to mark it explicitly as ESP.
As usual, I wanted to keep the old system usable and independent, so I have set this one on a separate disk, with a separate ESP, which I had to add (about 500 MB in size); the installer presented a warning about possibly making other systems hard to boot into if EFI is forced, but I've installed it on a separate disk (and adjusted UEFI boot priorities accordingly), so it was fine.
I used btrfs for a while, but decided to go with ext4 this time, since I use btrfs's advanced features less and less, while a simpler filesystem may be more reliable. Decided to minimize dealing with partitioning in the installer, and just made a single 500 GB partition for everything (not counting ESP, and while having 1.5 TB unpartitioned on the disk). No swap partition either, since in my experience it's not helpful and only freezes the system when something goes wrong. Didn't choose a network mirror to download new packages either, so the installation went quickly and smoothly.
      While the en_US.UTF_8 locale is very
      common, C.UTF_8 may be better to set at once, since
      it has 24-hour time format, sensible string sorting, and DBMSes
      (particularly PostgreSQL) are more portable when set with it,
      not running into collation version mismatches on replication
      between databases hosted on different operating systems. This is
      simply adjusted in /etc/default/locale.
    
As with CentOS about 7 years prior to this setup, apparently the nouveau driver was causing the system to freeze, so I installed the NVIDIA Proprietary Driver.
      Then I've added my user into the sudo group, have
      set the keyboard layout to colemak with sudo
      dpkg-reconfigure keyboard-configuration (since the
      installer doesn't provide that option), have set it in Xfce's
      settings to use the system layout (actually in a couple of
      places, not sure why there are so many). While at it, removed
      the useless bottom panel (application launcher), have set a dark
      theme, nicer icons, disabled icons on the desktop.
    
      As with servers, and perhaps more importantly than with those,
      decent and varied nameservers should be set. In this
      case /etc/resolv.conf mentions that it's generated
      by NetworkManager (which is rather awkward and unnecessary, and
      an example of little bloat task-xfce-desktop
      pulls), so one can adjust nameservers with nm-connection-editor.
    
      Then I've set the previously mentioned
      encrypted /home (this method is a bit verbose,
      since I've checked that things work as intended):
    
sudo fdisk /dev/sda # created another 500 GB partition for /home, sda3 sudo apt install cryptsetup sudo cryptsetup luksFormat /dev/sda3 sudo cryptsetup luksOpen /dev/sda3 enchome sudo mkfs.ext4 -L home /dev/mapper/enchome sudo cryptsetup close enchome sudo blkid | grep sda3 sudo -e /etc/crypttab # added the following: # enchome UUID=PARTITION_UUID_HERE none luks sudo -e /etc/fstab # added the following: # /dev/mapper/enchome /mnt/home ext4 defaults 0 2
      Then rebooted to ensure that /mnt/home mounts fine,
      moved the files from /home there (with cp
      -a), renamed /home, have
      set fstab to mount it
      into /home. Rebooted again, checked again that
      everything is fine, and removed the old /home.
    
      One may also mount /tmp into memory, reducing the
      data leaking to the unencrypted root filesystem, slightly
      speeding up some tasks, and reducing disk usage; it works for me
      and I like it, but there is plenty of criticizm and possible
      issues with that:
    
tmpfs /tmp tmpfs size=1g,nosuid 0 0
      Moved/imported my SSH and GPG keys, ~/.authinfo,
      some other files.
    
      I had to remap the "menu" key (keycode 135) to left alt, which
      is always awkward and different; in Xfce I had to enter the GUI
      settings, then "session and startup", and add the xmodmap
      -e "keycode 135 = Alt_L" command there. Also had to unmap
      C-M-f to be able to use it in Emacs, in "settings" - "keyboard"
      - "application shortcuts".
    
XFCE's default key bindings for basic tiling functionality aim a numpad, which I do not have, but those can be adjusted in "settings" - "window manager" - "keyboard".
      To disable GnuPG's annoying requirment to use non-alpha
      characters in a passphrase (which is contrary to NIST SP
      800-63B, and complains about passwords in the style of XKCD
      #936, such as those generated with xkcdpass), echo
      'min-passphrase-nonalpha 0' >>
      ~/.gnupg/gpg-agent.conf.
    
      More software: sudo apt install emacs
      emacs-common-non-dfsg telnet vlc tor mu4e isync rsync xsltproc
      clementine git elpa-magit elpa-haskell-mode cabal-install lynx
      whois nmap ncat dnsutils knot-dnsutils tmux fbreader inkscape
      blender godot3 gimp darktable lmms musescore texlive
      texlive-plain-generic auctex texlive-latex-extra texlive-science
      python3-sympy octave octave-symbolic libxml2-utils
      jmtpfs xkcdpass,
      and better-defaults, mu4e-alert,
      and cdlatex via Emacs's package manager (since they
      weren't in the system repositories). Generally it's a good idea
      to stick to a single package manager, since then you shouldn't
      run into version mismatches. update-alternatives --config
      editor to set vim as the default editor (running a new
      emacs instance may be a bit slow for quick sudo -e
      editos, emacsclient won't always work, setting a small emacs
      clone just for that seems excessive, and the default nano is
      just awkward, so vim is an okay option; though perhaps one can
      also set emacs -Q -nw). Over time a bunch of other
      things were added, including mpd (running as a user service) and
      mpc, strongSwan, likely more development tools.
    
      Then I set xterm and Emacs themes (.Xresources,
      Elisp), from my dotfiles repository.
    
      By 2022, I had to start using Tor bridges (since Tor is being
      blocked around here, and Internet connectivity is crippled in
      general, with Tor helping to fix some of it):
      install obfs4proxy, then append
      to /etc/tor/torrc:
    
UseBridges 1 ClientTransportPlugin obfs4 exec /usr/bin/obfs4proxy managed
      And bridge records received from bridges.torproject.org or by
      other means, prefixed with "Bridge" (Bridge obfs4
      ...). Though by 2024, many of those are blocked.
    
Configured Firefox: Sans Serif font, disallowed pages to choose their own fonts, increasing monospace font size to be the same as others (16), setting a minimal font size equal to those, "wp" keyword for Wikipedia search and "wt" for Wiktionary search, installing uBlock Origin (with "annoyance" lists additionally enabled) to cut out junk, NoScript to cut out more junk, FoxyProxy to use Tor for websites blacklisted around here and the ones I don't want to track me, HTTPS everywhere to mitigate local data retention practices (superceded by the Firefox's built-in HTTPS-Only Mode, which should be enabled in settings), Stylus to set a global dark theme for comfortable browsing when it is dark around.
      Configured isync and Emacs, later installed rexmpp's
      xmpp.el. Attempted a minimal Emacs configuration this time
      (though most likely it'll grow), so used the built-in rcirc
      (with rcirc-track-minor-mode and just
      setting rcirc-server-alist), not much of mu4e
      configuration. Something like this:
    
(require 'package)
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)
(package-initialize)
(require 'better-defaults)
(global-set-key [mode-line mouse-4] 'previous-buffer)
(global-set-key [mode-line mouse-5] 'next-buffer)
;; https://github.com/defanor/cyrillic-colemak
(require 'cyrillic-colemak)
(add-to-list 'custom-theme-load-path "~/.emacs.d/elisp/")
(load-theme 'blueish t)
(setq org-preview-latex-default-process 'dvisvgm
      org-babel-python-command "python3"
      org-src-preserve-indentation t)
(with-eval-after-load 'org
  (plist-put org-format-latex-options :scale 1.5)
  (require 'ob-python))
(rcirc-track-minor-mode t)
(setq rcirc-buffer-maximum-lines 2000
      rcirc-server-alist
      '(("irc.libera.chat" :port 6697 :encryption tls
         :user-name "defanor" :channels ("#emacs")))
      rcirc-authinfo
      '(("libera.chat" sasl "defanor" "password-here")))
(require 'haskell-interactive-mode)
(require 'haskell-process)
(add-hook 'haskell-mode-hook 'interactive-haskell-mode)
(add-hook 'haskell-mode-hook 'haskell-decl-scan-mode)
(require 'html-wysiwyg)
(add-hook 'html-mode-hook 'html-wysiwyg-mode)
(add-hook 'after-init-hook #'mu4e-alert-enable-mode-line-display)
(setq mail-user-agent 'mu4e-user-agent
      read-mail-command 'mu4e)
(with-eval-after-load "mu4e"
  (require 'smtpmail)
  (setq mml-secure-openpgp-encrypt-to-self t)
  (defun suppress-messages (old-fun &rest args)
    (cl-flet ((silence (&rest args1) (ignore)))
      (advice-add 'message :around #'silence)
      (unwind-protect
          (apply old-fun args)
        (advice-remove 'message #'silence))))
  (advice-add 'mu4e-update-mail-and-index :around #'suppress-messages)
  (advice-add 'mu4e-index-message :around #'suppress-messages)
  (advice-add 'progress-reporter-done :around #'suppress-messages)
  (setq mu4e-change-filenames-when-moving t)
(add-to-list
   'mu4e-contexts
   (make-mu4e-context
    :name "thunix"
    :enter-func (lambda ()
                  (mu4e-message "Switch to the thunix IMAP context")
                  ;; (mu4e~request-contacts)
                  )
    :leave-func (lambda () (mu4e-clear-caches))
    :match-func (lambda (msg)
                  (when msg
                    (mu4e-message-contact-field-matches
                     msg
                     :to "defanor@thunix.net")))
    :vars '( (user-mail-address            . "defanor@thunix.net")
             (user-full-name               . "defanor")
             (smtpmail-default-smtp-server . "thunix.net")
             (smtpmail-local-domain        . "thunix.net")
             (smtpmail-smtp-user           . "defanor")
             (smtpmail-smtp-server         . "thunix.net")
             (smtpmail-stream-type         . starttls)
             (smtpmail-smtp-service        . 25)
             (message-send-mail-function   . message-smtpmail-send-it)
             (mu4e-get-mail-command        . "mbsync -q thunix")
             (mu4e-update-interval         . 300)
             (mu4e-view-show-addresses     . t)
             (mu4e-maildir                 . "~/Maildir/thunix/")
             (mu4e-mu-home                 . "~/.mu/thunix")
             (mu4e-user-mail-address-list  . ("defanor@thunix.net"))
             )))
;; more contexts here
)
      And .mbsyncrc records like this:
    
IMAPAccount thunix Host thunix.net Port 993 User defanor SSLType IMAPS Pass "password-here" AuthMechs * IMAPStore thunix-remote Account thunix MaildirStore thunix-local Path ~/Maildir/thunix/ Inbox ~/Maildir/thunix/inbox/ Channel thunix Far :thunix-remote: Near :thunix-local: Patterns * !drafts Create Both Remove Both Expunge Both SyncState *
Then mu stores can be initialized with commands like mu
        init --muhome=~/.mu/thunix --maildir=~/Maildir/thunix
        --my-address=defanor@thunix.net.
      This was a sufficient setup to listen to a radio (vlc
      'http://s3.radionetz.de/1a-rock.mp3'; as of 2025-10-27,
      that is blocked here, so one could use something like vlc
      'http://113fm.cdnstream1.com/1740_128' instead;
      see dir.xiph.org for other online radios), local music
      collection (which I keep on a separate partition, so just
      mounted it via fstab into the same path as before,
      and the playlist also stored on it contained correct paths),
      communicate (IRC, XMPP, email), do Haskell programming, browse
      WWW relatively comfortably, play Discworld MUD over telnet, and
      publish these notes. At that point I've adjusted dwproxy to be
      able to build it using only dependencies from the system
      repositories (for related rants and musings, see the notes
      on software packaging and deployment and everyday programming in
      Haskell), and built a few work projects: since it's Cabal 3 now,
      had to set cabal.project in order to use internal libraries, and
      made some other minor adjustments to handle newer versions of
      dependencies. C projects (rexmpp in particular) also required
      minor adjustments to handle newer versions of the compiler and
      libraries, but fairly straightforward.
    
      Realtime Policy and Watchdog Daemon (rtkit) can be quite spammy
      in the logs with its debug messages, but that can be fixed by
      overriding its systemd service (sudo systemctl edit
      rtkit-daemon.service, followed by sudo systemctl
      daemon-reload and sudo systemctl restart
      rtkit-daemon.service to apply it) with the following:
    
[Service] LogLevelMax=info
      Following the instructions (Chapter 4. Upgrades from Debian 11
      (bullseye)), I executed apt full-upgrade to
      find out that my graphics card (GTX 660) is not supported by the
      NVIDIA proprietary driver anymore. Chose to not install the new
      nvidia-driver, but that interrupted the process, so had
      to apt --fix-broken install, and then apt
      full-upgrade again. Afterwards
      removed nvidia-driver,
      chose mesa-diverted in update-glx --config
      glx in order to de-blacklist nouveau drivers, rebooted,
      the system only worked for some minutes before freezing,
      rendering it unusable. Fortunately I have integrated graphics
      here (Xeon E3-1275 v2 on ASUS P8C WS), which I picked precisely
      because this sort of thing keeps happening; took the graphics
      card out, connected the display to the motherboard's DVI
      output. Apparently I disconnected the system disk while taking
      the graphics card out, so failed to boot; then reconnected it,
      and saw it via UEFI, but failed to boot still, with different
      priorities (possibly messed up the UEFI boot settings while
      poking them without the disk connected properly). Managed to
      boot into the system by booting grub from a live USB stick, then
      pointing it to the system's grub.cfg using grub shell's
      configfile command. Tried to fix it with
      efibootmgr, that did not work, but it worked to just
      do grub-install and update-grub,
      leading to a working system into which I can boot directly,
      albeit without a graphics card. See GrubEFIReinstall for more
      options.
    
Additionally, some texlive packages failed to update, and some fcitx5 ones were kept back.
      Afterwards I did apt autoremove, which removed
      telnet, so had to apt install telnet again.
    
      mu4e broke as well: had to update mu4e-alert via Emacs, since it
      came from melpa, but then it kept failing with "Mu server
      process ended with exit code 1". Dug the approximate command out
      of the sources (/usr/bin/mu server --debug
      --muhome=~/.mu/thunix), executed it manually, saw the
      error message: "error: expected schema-version 465, but got 451;
      cannot auto-upgrade; please use 'mu init'", "Please
      (re)initialize mu with 'mu init' see mu-init(1) for
      details". Did mv ~/.mu/ ~/.mu-old/, then mu
      init --muhome=~/.mu/thunix --maildir=~/Maildir/thunix
      --my-address=defanor@thunix.net (and similar ones, for
      other mailboxes), and then it worked. As many other programs,
      mbsync deprecated "master/slave" terminology, introducing its
      unique alternative: "far/near".
    
      Had to M-x customize-group RET ansi-colors RET,
      since ansi-color-names-vector became obsolete.
    
      I had an unused PostgreSQL 13 (used primarily for local
      testing), and PostgreSQL 15 was installed by the system upgrade,
      so I just cleaned up the old version: sudo pg_dropcluster
      --stop 13 main, sudo apt remove
      postgresql-13 postgresql-client-13.
    
      Then I was left with a bunch of other "installed,local" packages
      (apt list '?narrow(?installed,
      ?not(?origin(Debian)))'), so cleaned some of those up,
      after checking that they do not seem to be necessary: sudo
      apt remove haskell-platform gcc-10 gcc-9-base gcc-10-base
      clang-11 python-numpy-doc openjdk-11-jre openjdk-11-jdk
      openjdk-11-jre-headless openjdk-11-jdk-headless libx264-160
      libx265-192 libwebp6 libvpx6 libswresample3 libssl1.1 libsepol1
      firmware-intelwimax linux-image-5.10.0-8-amd64
      linux-image-5.10.0-23-amd64 iukrainian libffi7 libbpf0
      libprocps8.
    
Had to use a workaround for the FBReader's hyphenation-after-each-word bug.
It is handy to host servers locally, particularly for communication: they are always available from the primary system then, the latency is reduced, regular TLS allows for peer-to-peer connections. As a downside, issues with the primary system also lead to downtime of those.
      Eventually I decided that having a properly configured XMPP
      server locally is useful as a backup, for lower-latency calls,
      and to decrease load on remote servers. Having just an A record
      pointing to my static IP address (a free dyndns service in this
      case, to avoid dependencies on domain names at once), and port
      forwarding configured on the router for ports 80, 5222, 5269,
      5281, 3478, 49152-49155, I have set nginx and uacme to obtain an
      X.509 certificate for TLS, configured nftables to decrease spam
      in the logs (only accepting connections on port 80 when renewing
      a certificate), then configured Prosody and coturn. sudo
      apt install nginx uacme nftables prosody
      coturn. My /etc/nftables.conf, slightly
      abridged to focus on relevant parts:
    
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
  set not-clients {
    type ipv4_addr
    flags interval
    elements = { 1.0.0.0/8 }
  }
  set blocks {
    type ipv4_addr
    flags interval
    elements = { 1.1.1.1 }
  }
  set open-ports-s2s {
    type inet_service
    flags interval
    elements = { 5269 }
  }
  set open-ports-c2s {
    type inet_service
    flags interval
    elements = { 5222, 5281, 3478, 49152-49155 }
  }
  chain input {
    type filter hook input priority 0; policy drop;
    # Mitigate TCP reset attacks performed by the ISP.
    ip saddr @blocks tcp sport 443 tcp flags rst drop;
    # Allow traffic from established and related packets.
    ct state established,related accept
    # Allow loopback traffic.
    iifname lo accept
    # Allow incoming TCP and UDP packets on @open-ports-s2s.
    tcp dport @open-ports-s2s accept;
    udp dport @open-ports-s2s accept;
    # Drop connections from spammy addresses.
    ip saddr @not-clients drop;
    # Allow incoming TCP and UDP packets on @open-ports-c2s.
    tcp dport @open-ports-c2s accept;
    udp dport @open-ports-c2s accept;
  }
  chain forward {
    type filter hook forward priority 0;
  }
  chain output {
    type filter hook output priority 0;
  }
}
      Then set /usr/local/bin/uacme-hook.sh,
      modifying /usr/share/uacme/uacme.sh:
    
--- /usr/share/uacme/uacme.sh   2023-02-15 23:31:43.000000000 +0300
+++ /usr/local/bin/uacme-hook.sh        2024-01-30 09:49:06.505761694 +0300
@@ -16,7 +16,7 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-CHALLENGE_PATH="${UACME_CHALLENGE_PATH:-/var/www/.well-known/acme-challenge}"
+CHALLENGE_PATH="${UACME_CHALLENGE_PATH:-/var/www/html/.well-known/acme-challenge}"
 ARGS=5
 E_BADARGS=85
 
@@ -37,6 +37,8 @@
         case "$TYPE" in
             http-01)
                 printf "%s" "${AUTH}" > "${CHALLENGE_PATH}/${TOKEN}"
+                # Temporarily allow connections to port 80
+                sudo nft add element inet filter open-ports-s2s {80}
                 exit $?
                 ;;
             *)
@@ -48,7 +50,10 @@
     "done"|"failed")
         case "$TYPE" in
             http-01)
+                sudo nft delete element inet filter open-ports-s2s {80}
                 rm "${CHALLENGE_PATH}/${TOKEN}"
                 exit $?
                 ;;
             *)Then:
sudo mkdir -p /var/www/html/.well-known/acme-challenge
sudo mkdir /etc/prosody/certs/example.com/
sudo touch /etc/prosody/certs/example.com/{fullchain,privkey}.pem
sudo chmod 640 /etc/prosody/certs/example.com/{fullchain,privkey}.pem
sudo chown root:prosody /etc/prosody/certs/example.com/{fullchain,privkey}.pem
sudo uacme -v new
sudo uacme -h /usr/local/bin/uacme-hook.sh issue example.com
sudo -e /etc/cron.daily/uacme-cert-update
sudo chmod +x /etc/cron.daily/uacme-cert-updateWith the following in /etc/cron.daily/uacme-cert-update:
#!/bin/sh set -e /usr/bin/uacme -h /usr/local/bin/uacme-hook.sh issue example.com cp /etc/ssl/uacme/example.com/cert.pem /etc/prosody/certs/example.com/fullchain.pem cp /etc/ssl/uacme/private/example.com/key.pem /etc/prosody/certs/example.com/privkey.pem
In /etc/turnserver.conf I have only set external-ip, static-auth-secret, use-auth-secret, max-port=49154.
Relevant lines of /etc/prosody/prosody.cfg.lua:
interfaces = { "192.168.1.8", "127.0.0.1", "::1" }
modules_enabled = {
--- [...]
	-- Other modules
                "turn_external";
                "http";
}
-- TURN
turn_external_host = "example.com"
turn_external_secret = "secret here"
-- HTTP
http_host = "example.com"
VirtualHost "example.com"
Component "upload.example.com" "http_file_share"
      Then restart or reload the services, add users with sudo
      prosodyctl adduser <jid>, and it works.
    
      For voice conferences, apparently a particularly easy to set and
      properly working option is Mumble. sudo apt install
      mumble-server mumble, set a password
      in /etc/mumble-server.init, open UDP and TCP ports,
      and it is ready to use with desktop clients or Mumla or Android.
    
Similarly to XMPP and voice conferences, one may set an IRC server (or a small network) for private chatting. InspIRCd is available from Debian repositories and easy to configure, simply by setting the desired hosts, names, and passwords in its configuration file. And links (the spanningtree module) for use with multiple servers. Anope IRC services seem popular, and also available from Debian repositories, but perhaps unnecessary for a small private (and possibly local) network. To make it available over Internet, one may want to both enforce TLS and add restrictions for those connection classes; to do so, one may define a single connection class allowing no connections, then inherit one for plain connections, and one for TLS connections on a different port (corresponding to the Internet-facing endpoint), with additional restrictions (e.g., requiring a password).