Debian 11 (to 12) workstation

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.


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.

Initial setup

As with CentOS about 7 years ago, 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".

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 python3-sympy octave octave-symbolic, 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): install obfs4proxy, then append to /etc/tor/torrc:

UseBridges 1
ClientTransportPlugin obfs4 exec /usr/bin/obfs4proxy managed

And bridge records received from or by other means, prefixed with "Bridge" (Bridge obfs4 ...).

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" . "") t)

(require 'better-defaults)
(global-set-key [mode-line mouse-4] 'previous-buffer)
(global-set-key [mode-line mouse-5] 'next-buffer)

(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
      '(("" :port 6697 :encryption tls
         :user-name "defanor" :channels ("#emacs"))
        ("" :port 1500 :encryption tls
         :password "password-here"))
      '(("" 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)
          (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)
    :name "uberspace"
    :enter-func (lambda ()
                  (mu4e-message "Switch to the uberspace IMAP context")
                  ;; (mu4e~request-contacts)
    :leave-func (lambda () (mu4e-clear-caches))
    :match-func (lambda (msg)
                  (when msg
                     :to "")))
    :vars '( (user-mail-address            . "")
             (user-full-name               . "defanor")
             (smtpmail-default-smtp-server . "")
             (smtpmail-local-domain        . "")
             (smtpmail-smtp-user           . "defanor")
             (smtpmail-smtp-server         . "")
             (smtpmail-stream-type         . starttls)
             (smtpmail-smtp-service        . 587)
             (message-send-mail-function   . message-send-mail-with-sendmail)
             (mu4e-get-mail-command        . "mbsync -q uberspace")
             (mu4e-update-interval         . 300)
             (mu4e-view-show-addresses     . t)
             (mu4e-maildir                 . "~/Maildir/uberspace/")
             (mu4e-mu-home                 . "~/.mu/uberspace")
             (mu4e-user-mail-address-list  . (""))
;; more contexts here

And .mbsyncrc records like this:

IMAPAccount uberspace
Port 993
User defanor
Pass password-here
AuthMechs *

IMAPStore uberspace-remote
Account uberspace

MaildirStore uberspace-local
Path ~/Maildir/uberspace/
Inbox ~/Maildir/uberspace/inbox/

Channel uberspace
Master :uberspace-remote:
Slave :uberspace-local:
Patterns * !drafts
Create Both
Remove Both
Expunge Both
SyncState *

Then mu stores can be initialized with commands like mu init --muhome=~/.mu/uberspace --maildir=~/Maildir/uberspace

This was a sufficient setup to listen to a radio (vlc ''), 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:


Update to Debian 12

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/uberspace), 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/uberspace --maildir=~/Maildir/uberspace (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.

XMPP server

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 = { }
  set blocks {
    type ipv4_addr
    flags interval
    elements = { }
  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/, modifying /usr/share/uacme/

--- /usr/share/uacme/   2023-02-15 23:31:43.000000000 +0300
+++ /usr/local/bin/        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 <>.
@@ -37,6 +37,8 @@
         case "$TYPE" in
                 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 @@
         case "$TYPE" in
+                sudo nft delete element inet filter open-ports-s2s {80}
                 rm "${CHALLENGE_PATH}/${TOKEN}"
                 exit $?


sudo mkdir -p /var/www/html/.well-known/acme-challenge
sudo mkdir /etc/prosody/certs/
sudo touch /etc/prosody/certs/{fullchain,privkey}.pem
sudo chmod 640 /etc/prosody/certs/{fullchain,privkey}.pem
sudo chown root:prosody /etc/prosody/certs/{fullchain,privkey}.pem
sudo uacme -v new
sudo uacme -h /usr/local/bin/ issue
sudo -e /etc/cron.daily/uacme-cert-update
sudo chmod +x /etc/cron.daily/uacme-cert-update

With the following in /etc/cron.daily/uacme-cert-update:

set -e
/usr/bin/uacme -h /usr/local/bin/ issue
cp /etc/ssl/uacme/ /etc/prosody/certs/
cp /etc/ssl/uacme/private/ /etc/prosody/certs/

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 = { "", "", "::1" }
modules_enabled = {
--- [...]
	-- Other modules
turn_external_host = ""
turn_external_secret = "secret here"

http_host = ""

VirtualHost ""

Component "" "http_file_share"

Then restart or reload the services, add users with sudo prosodyctl adduser <jid>, and it works.

Voice conferences

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.