~/ terminal notes

Arch Linux Minimal Home Server
— ThinkPad T480

This is a living document. Everything below is copy-paste ready and tested on a fresh Arch install. No fluff — only what I actually run. A fresh install should reach full working state in under 30 minutes .

Every package installed has a reason.
Everything is a file.
Config lives in ~/.dotfiles, version-controlled on GitHub.
Reproducibility.
Fresh install → full working state in < 30 min .
Terminal is home.
No GUI, no display manager. tmux + nvim is the entire IDE.

// hardware

ThinkPad T480 Overview

Hardware

Before touching a keyboard, know your machine.
ComponentT480 SpecNotes
CPUIntel Core i5/i7 8th GenGood Linux support, intel_pstate driver
RAMUp to 64GB DDR4 (2 slots)Max it out — ZFS or Docker loves RAM
NICIntel I219-LMExcellent kernel support, use for server NIC
WiFiIntel 8265iwlwifi driver, works OOTB
StorageNVMe M.2 + 2.5" SATAUse NVMe for OS, SATA for data if available
Display14" IPSIrrelevant for a headless server

BIOS Settings

Change these before install:

SettingValue
Security › Secure BootDisabled
Config › Network › Wake on LANEnabled
Startup › UEFI/Legacy BootUEFI Only
Config › Thunderbolt BIOS Assist ModeEnabled

// part 1

Installation

Create Bootable USB

# Verify the ISO (ALWAYS do this)
gpg --keyserver-options auto-key-retrieve --verify archlinux-*.iso.sig

# Find your USB device — be careful, wrong device = data loss
lsblk

# Write the ISO (replace sdX with your USB device, e.g., sdb)
dd if=archlinux-*.iso of=/dev/sdX bs=4M status=progress oflag=sync

Boot the T480 with F12 to get the boot menu. Select your USB.

Pre-Installation: Network

Wired (recommended for install)

# Should come up automatically via DHCP
ip link
ping archlinux.org

Wireless (if needed)

iwctl
# Inside iwctl:
device list
station wlan0 scan
station wlan0 get-networks
station wlan0 connect "YourSSID"
exit

Console Keyboard & Clock

loadkeys us
timedatectl set-ntp true
timedatectl status

Disk Partitioning (UEFI + GPT)

Critical step. We use a simple 3-partition layout on NVMe.
PartitionSizeTypeMountPurpose
/dev/nvme0n1p1512MEFI System/boot/efiBootloader
/dev/nvme0n1p24GLinux swap[SWAP]Swap
/dev/nvme0n1p3RemainderLinux filesystem/Root
lsblk
gdisk /dev/nvme0n1

# Inside gdisk:
Command: o    # new GPT partition table
Proceed? y

# EFI partition
Command: n
Partition number: 1
First secrtor: (default - enter)
Last sector: +512M
Hex code: EF00

# Swap partition
Command: n
Partition number: 2
First secrtor: (default - enter)
Last sector: +4G
Hex code: 8200

# Root partition
Command: n
Partition number: 3
First secrtor: (default - enter)
Last sector: (default — uses all remaining space)
Hex code: 8300

Command: w    # write and exit
Confirm? y

Format & Mount

# EFI — must be FAT32
mkfs.fat -F32 /dev/nvme0n1p1

# Swap
mkswap /dev/nvme0n1p2
swapon /dev/nvme0n1p2

# Root — ext4 (reliable, battle-tested)
mkfs.ext4 /dev/nvme0n1p3
# Alternative: btrfs for CoW snapshots
# mkfs.btrfs /dev/nvme0n1p3

# Mount
mount /dev/nvme0n1p3 /mnt
mkdir -p /mnt/boot/efi
mount /dev/nvme0n1p1 /mnt/boot/efi
Why ext4? For a development server, ext4 is boring and correct. 20+ years of kernel hardening, zero surprises. Use btrfs if you specifically want CoW snapshots.

Select Fast Mirrors

cp /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak

reflector \
  --country India,Singapore,Germany \
  --protocol https \
  --sort rate \
  --latest 10 \
  --save /etc/pacman.d/mirrorlist

# For old ISO — update keyrings first
pacman -S archlinux-keyring

Install Base System

pacstrap -K /mnt \
  base \
  base-devel \
  linux \
  linux-firmware \
  linux-headers \
  intel-ucode \
  networkmanager \
  openssh \
  git \
  neovim \
  man-db \
  man-pages \
  texinfo
PackagePurpose
baseMinimal Arch userspace (bash, glibc, coreutils)
base-develBuild tools: gcc, make, binutils, fakeroot, sudo
linuxThe kernel
linux-firmwareFirmware blobs for WiFi, Bluetooth, etc.
linux-headersRequired to build DKMS kernel modules
intel-ucodeCritical. CPU microcode updates. Prevents obscure bugs and security holes.
networkmanagerNetwork management. Main network tool.
opensshNeeded for SSH.
gitNeeded for version controll and development.
nvimMy editor of choice.
man-db, man-pages, texinfoMan pages. Never skip these.

fstab, Chroot & Configure

# Generate fstab (-U uses UUIDs — stable even if you add/remove drives)
genfstab -U /mnt >> /mnt/etc/fstab
cat /mnt/etc/fstab   # VERIFY THIS. A wrong fstab bricks boot.

# Chroot into new system
arch-chroot /mnt

Timezone & Locale

ln -sf /usr/share/zoneinfo/Asia/Kolkata /etc/localtime
hwclock --systohc

nvim /etc/locale.gen
# Uncomment: en_US.UTF-8 UTF-8
locale-gen
echo "LANG=en_US.UTF-8" > /etc/locale.conf
echo "KEYMAP=us" > /etc/vconsole.conf

Hostname & Hosts

echo "t480-srv" > /etc/hostname

cat > /etc/hosts << 'EOF'
127.0.0.1   localhost
::1         localhost
127.0.1.1   t480-srv.localdomain t480-srv
EOF

Root Password & User

passwd   # set root password

# Create your user
useradd -m -G wheel,audio,video,storage,optical,network -s /bin/bash dev
passwd dev

# Grant sudo access to wheel group
EDITOR=nvim visudo
# Uncomment: %wheel ALL=(ALL:ALL) ALL

Bootloader GRUB

Why GRUB over systemd-boot ?
Because i couldn't get systemd-boot to work

Bootloader — GRUB

For when systemd-boot does not work:

pacman -S grub efibootmgr
grub-install \
  --target=x86_64-efi \
  --efi-directory=/boot/efi \
  --bootloader-id=GRUB
grub-mkconfig -o /boot/grub/grub.cfg
Skip to reboot if using GRUB

Bootloader —systemd-boot (fallback)

For when you can get systemd-boot to work:

bootctl install

# Get UUID of root partition
blkid /dev/nvme0n1p3

cat > /boot/loader/entries/arch.conf << 'EOF'
title   Arch Linux
linux   /vmlinuz-linux
initrd  /intel-ucode.img
initrd  /initramfs-linux.img
options root=UUID=YOUR-UUID-HERE rw quiet loglevel=3
EOF

cat > /boot/loader/loader.conf << 'EOF'
default  arch.conf
timeout  3
console-mode max
editor   no
EOF

Replace YOUR-UUID-HERE with the UUID from blkid. editor no disables runtime kernel parameter editing — a basic security measure.

Pacman Hook for Bootloader Updates

mkdir -p /etc/pacman.d/hooks
cat > /etc/pacman.d/hooks/95-systemd-boot.hook << 'EOF'
[Trigger]
Type = Package
Operation = Upgrade
Target = systemd

[Action]
Description = Gracefully upgrading systemd-boot...
When = PostTransaction
Exec = /usr/bin/systemctl restart systemd-boot-update.service
EOF

Enable Services & Reboot

systemctl enable NetworkManager
systemctl enable sshd

exit          # exit chroot
umount -R /mnt
reboot        # remove USB before or during reboot

// part 2

Post-Install System Configuration

Log in as your user (dev). The next steps harden and configure the base system.

Network with NetworkManager

ping archlinux.org   # verify network is up

nmcli device status
# Connect to WiFi if needed:
nmcli device wifi connect "SSID" password "password"

SSH Hardening

Set up SSH keys BEFORE disabling password auth. Lock yourself out and you'll need physical access.
sudo nvim /etc/ssh/sshd_config
DirectiveValueReason
Port2222Non-standard port, reduces script kiddie noise
PermitRootLoginnoNever allow root SSH login
PasswordAuthenticationnoKey-only auth
PubkeyAuthenticationyes
MaxAuthTries3
X11Forwardingno
AllowTcpForwardingno

Set Up SSH Keys

# On your client machine:
ssh-keygen -t ed25519 -C "your@email.com"

# Copy public key to server (while password auth is still on)
ssh-copy-id -p 2222 dev@t480-srv.local

# Test FIRST, then disable password auth
sudo systemctl restart sshd
ssh -p 2222 dev@t480-srv.local

Firewall with nftables

sudo pacman -S nftables
sudo nvim /etc/nftables.conf
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
  chain input {
    type filter hook input priority filter; policy drop;

    ct state established,related accept
    iif "lo" accept
    ip protocol icmp accept
    ip6 nexthdr icmpv6 accept

    # Allow SSH on custom port
    tcp dport 2222 accept

    log prefix "nft-drop: " drop
  }
  chain forward {
    type filter hook forward priority filter; policy drop;
  }
  chain output {
    type filter hook output priority filter; policy accept;
  }
}
sudo systemctl enable --now nftables
sudo nft list ruleset   # verify

AUR Helper — paru

paru is a modern, Rust-written AUR helper. From now on, use paru instead of pacman for everything — it handles both official repos and AUR transparently.

git clone https://aur.archlinux.org/paru.git /tmp/paru
cd /tmp/paru
makepkg -si
cd -

pacman Configuration

sudo nvim /etc/pacman.conf

# Uncomment/add:
Color
ParallelDownloads = 5

[multilib]
Include = /etc/pacman.d/mirrorlist

sudo pacman -Syyu

// part 3

Developer Toolchain

Shell — zsh + plugins

paru -S zsh zsh-completions zsh-autosuggestions zsh-syntax-highlighting starship
chsh -s /usr/bin/zsh

~/.zshrc

All config files are available in github
# History
HISTSIZE=10000
SAVEHIST=10000
HISTFILE=~/.zsh_history
setopt HIST_IGNORE_DUPS
setopt SHARE_HISTORY

# Completion
autoload -Uz compinit && compinit
zstyle ':completion:*' menu select

# Plugins
source /usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh
source /usr/share/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh

# Starship prompt
eval "$(starship init zsh)"

# Aliases
alias ls='ls --color=auto'
alias ll='ls -alh'
alias la='ls -A'
alias grep='grep --color=auto'
alias diff='diff --color=auto'
alias ip='ip --color=auto'
alias ..='cd ..'
alias ...='cd ../..'

# Exports
export EDITOR=nvim
export VISUAL=nvim
export PAGER=less
export LESS='-R --use-color'
export PATH="$HOME/.local/bin:$PATH"
Why zsh? Fish is prettier but non-POSIX. Bash is universal but ergonomically dated. Zsh is POSIX-compatible, has a massive plugin ecosystem.

Why starship? Cross-shell, written in Rust, zero config needed to look great, shows git status, language versions, and exit codes at a glance.

tmux — Terminal Multiplexer

Why tmux? On a server you SSH into, tmux is essential. Sessions persist when your SSH connection drops. Detach, walk away, come back, tmux attach to pick up exactly where you left.
paru -S tmux

~/.tmux.conf

# Change prefix Ctrl+B → Ctrl+A (screen-style)
unbind C-b
set-option -g prefix C-a
bind-key C-a send-prefix

# Reload config
bind r source-file ~/.tmux.conf \; display "Config reloaded!"

# Split panes
bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}"
unbind '"'
unbind %

# Navigate panes with Alt+Arrow (no prefix)
bind -n M-Left  select-pane -L
bind -n M-Right select-pane -R
bind -n M-Up    select-pane -U
bind -n M-Down  select-pane -D

# Mouse support
set -g mouse on

# Start windows at 1, not 0
set -g base-index 1
setw -g pane-base-index 1
set -g renumber-windows on

# Scrollback
set -g history-limit 50000

# Status bar (catppuccin mocha colors)
set -g status-position bottom
set -g status-style 'bg=#1e1e2e fg=#cdd6f4'
set -g status-left '#[fg=#89b4fa,bold] #S '
set -g status-right '#[fg=#a6e3a1] %Y-%m-%d %H:%M '
set -g window-status-current-style 'fg=#f38ba8,bold'

# 256 color support
set -g default-terminal "tmux-256color"
set -ag terminal-overrides ",xterm-256color:RGB"

# Activity monitoring
setw -g monitor-activity on
set -g visual-activity off
KeyAction
C-a ddetach (session keeps running)
C-a cnew window
C-a ,rename window
C-a |split vertical
C-a -split horizontal
C-a [scroll mode (q to exit)

Neovim — Editor

paru -S neovim python-pynvim nodejs npm ripgrep fd fzf lazygit

Config Structure

~/.config/nvim/
├── init.lua
└── lua/
    ├── options.lua
    ├── keymaps.lua
    ├── autocmds.lua
    └── plugins/
        ├── init.lua      # lazy.nvim bootstrap
        ├── lsp.lua
        ├── telescope.lua
        ├── treesitter.lua
        └── ui.lua

init.lua

require("options")
require("keymaps")
require("autocmds")
require("plugins")

lua/options.lua

local opt = nvim.opt

opt.number = true
opt.relativenumber = true
opt.tabstop = 4
opt.shiftwidth = 4
opt.expandtab = true
opt.smartindent = true
opt.ignorecase = true
opt.smartcase = true
opt.termguicolors = true
opt.signcolumn = "yes"
opt.cursorline = true
opt.scrolloff = 8
opt.wrap = false
opt.colorcolumn = "100"
opt.undofile = true    -- persistent undo (survives nvim restart)
opt.swapfile = false
opt.updatetime = 250
opt.clipboard = "unnamedplus"

lua/keymaps.lua (selected)

local map = nvim.keymap.set
vim.g.mapleader = " "

-- File operations
map("n", "<leader>w", "<cmd>write<cr>",       { desc = "Save file" })
map("n", "<leader>e", "<cmd>Neotree toggle<cr>", { desc = "File tree" })

-- Telescope
map("n", "<leader>ff", "<cmd>Telescope find_files<cr>", { desc = "Find files" })
map("n", "<leader>fg", "<cmd>Telescope live_grep<cr>",  { desc = "Grep project" })

-- LSP
map("n", "gd", nvim.lsp.buf.definition, { desc = "Go to definition" })
map("n", "K",  nvim.lsp.buf.hover,       { desc = "Hover docs" })
map("n", "<leader>rn", nvim.lsp.buf.rename, { desc = "Rename" })

-- Lazygit
map("n", "<leader>gg", "<cmd>terminal lazygit<cr>i", { desc = "LazyGit" })

LSP Servers (mason)

require("mason-lspconfig").setup({
  ensure_installed = {
    "lua_ls", "pyright", "ts_ls",
    "bashls", "dockerls", "yamlls",
  },
})

Development Languages & Runtimes

# Python (uv — Rust-based, dramatically faster than pip)
paru -S python python-pip python-virtualenv uv

# Node.js (nvm for version management)
paru -S nvm
# Add to .zshrc: source /usr/share/nvm/init-nvm.sh
nvm install --lts && nvm use --lts

# Go
paru -S go

# Rust (official way — do not use pacman for rustup)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env

# Docker
paru -S docker docker-compose
sudo systemctl enable --now docker
sudo usermod -aG docker dev

# Modern CLI tools
paru -S bat eza zoxide git-delta btop tldr jq yq httpie ncdu duf direnv

Git Configuration

git config --global user.name  "Your Name"
git config --global user.email "you@example.com"
git config --global core.editor nvim
git config --global init.defaultBranch main
git config --global pull.rebase true
git config --global core.pager delta

# delta (beautiful diffs)
git config --global delta.navigate true
git config --global delta.side-by-side true

# Aliases
git config --global alias.lg  "log --oneline --graph --decorate --all"
git config --global alias.st  status
git config --global alias.co  checkout
git config --global alias.undo "reset HEAD~1 --mixed"

SSH Keys for GitHub

ssh-keygen -t ed25519 -C "you@example.com" -f ~/.ssh/github

cat >> ~/.ssh/config << 'EOF'
Host github.com
  HostName github.com
  User git
  IdentityFile ~/.ssh/github
  AddKeysToAgent yes
EOF
chmod 600 ~/.ssh/config

cat ~/.ssh/github.pub  # paste into GitHub → Settings → SSH Keys
ssh -T git@github.com  # verify
Add to .zshrc to autoload agent
# Start ssh-agent only if not running
# Ensure SSH agent is running and connected
if [ -z "$SSH_AUTH_SOCK" ]; then
  eval "$(ssh-agent -s)" > /dev/null 2>&1
fi

# Add key if not already added
if ! ssh-add -l 2>/dev/null | grep -q "$(ssh-keygen -lf ~/.ssh/github.pub | awk '{print $2}')"; then
  ssh-add ~/.ssh/github > /dev/null
fi

Development Services

# Redis
paru -S redis
sudo systemctl enable --now redis

# Nginx / Caddy (reverse proxy)
paru -S nginx        # or caddy for automatic HTTPS
sudo systemctl enable --now nginx

// part 4

Server Hardening & Maintenance

Hardening

Do not auto-update blindly on Arch — it's a rolling release and major updates can break things. Get notified instead.

Security Audit

paru -S arch-audit
arch-audit   # check for vulnerable packages

# Auto-clean old package cache
paru -S pacman-contrib
sudo systemctl enable --now paccache.timer

fail2ban

paru -S fail2ban

sudo cat > /etc/fail2ban/jail.local << 'EOF'
[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 3

[sshd]
enabled = true
port    = 2222
EOF

sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd

Wake-on-LAN

NOT YET TESTED
# Enable WoL (persisted via systemd service)
sudo cat > /etc/systemd/system/wol.service << 'EOF'
[Unit]
Description=Enable Wake-on-LAN
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/sbin/ethtool -s enp0s31f6 wol g

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable --now wol.service

# From any machine on the network:
wakeonlan AA:BB:CC:DD:EE:FF

Auto-attach tmux on SSH

# Add to ~/.zshrc:
if [[ -n "$SSH_CONNECTION" ]] && command -v tmux &>/dev/null; then
  [[ -z "$TMUX" ]] && exec tmux new-session -A -s main
fi

// part 5

Dotfiles Management with Git

The Bare Repository Method

The cleanest approach — no symlinks, no extra tools. A bare git repository that tracks your home directory directly.

# Create the bare repo
git init --bare ~/.dotfiles

# Create the alias (add to .zshrc IMMEDIATELY)
alias dot='git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME'

# Don't show untracked files (home dir has thousands)
dot config --local status.showUntrackedFiles no

# Set your remote
dot remote add origin git@github.com:Manav-madhu/dotfiles.git

The dot alias is just git pointed at a bare repo in ~/.dotfiles with the working tree set to $HOME. Track any file in your home directory without moving it.

Track Config Files

dot add ~/.zshrc
dot add ~/.tmux.conf
dot add ~/.gitconfig
dot add ~/.ssh/config
dot add ~/.config/nvim/init.lua
dot add ~/.config/nvim/lua/options.lua
dot add ~/.config/nvim/lua/keymaps.lua
dot add ~/.config/nvim/lua/plugins/init.lua
dot add ~/.config/starship.toml

dot commit -m "feat: initial dotfiles setup"
dot push -u origin main

Repo Structure

dotfiles (bare repo, visualized)
├── .zshrc
├── .tmux.conf
├── .gitconfig
├── .ssh/
│   └── config
├── .config/
│   ├── nvim/
│   │   ├── init.lua
│   │   └── lua/
│   │       ├── options.lua
│   │       ├── keymaps.lua
│   │       ├── autocmds.lua
│   │       └── plugins/
│   └── starship.toml
└── .local/
    └── bin/          # personal scripts

Daily Workflow

dot status          # check tracked files
dot diff            # see what changed
dot add ~/.zshrc    # track a new/changed file
dot commit -m "feat: add X to zshrc"
dot push
dot pull            # sync on another machine

Useful aliases to add to .zshrc:

alias dot='git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME'
alias dots='dot status'
alias dotd='dot diff'
alias dotl='dot log --oneline -20'
alias dotp='dot push'

Bootstrap Script

THIS IS A AI Generated script use at your own risk!!!

Restores your entire environment on a fresh Arch install. Save as ~/.local/bin/bootstrap.sh and track it in dotfiles.

!! change dotfile repo variable !!

#!/usr/bin/env bash
# bootstrap.sh — Full system setup from dotfiles
set -euo pipefail

echo "==> Starting bootstrap..."

# Install paru if not present
if ! command -v paru &>/dev/null; then
  git clone https://aur.archlinux.org/paru.git /tmp/paru
  cd /tmp/paru && makepkg -si --noconfirm && cd -
fi

PACKAGES=(
  zsh zsh-completions zsh-autosuggestions zsh-syntax-highlighting starship
  tmux
  neovim python-pynvim nodejs npm ripgrep fd fzf lazygit
  bat eza zoxide git-delta btop tldr jq yq httpie ncdu duf direnv
  python python-pip uv go rustup
  docker docker-compose postgresql redis nginx
  fail2ban nftables avahi nss-mdns networkmanager
)
paru -S --needed --noconfirm "${PACKAGES[@]}"

# Restore dotfiles
DOTFILES_REPO="git@github.com:Manav-madhu/dotfiles.git"
if [ ! -d "$HOME/.dotfiles" ]; then
  git clone --bare "$DOTFILES_REPO" "$HOME/.dotfiles"
fi
alias dot='git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME'
dot checkout
dot config --local status.showUntrackedFiles no

# Set default shell
chsh -s /usr/bin/zsh

# Enable services
sudo systemctl enable --now NetworkManager sshd nftables fail2ban docker avahi-daemon
sudo usermod -aG docker "$USER"

echo ""
echo "==> Bootstrap complete!"
echo "  1. Re-login for group changes (docker)"
echo "  2. Open nvim and run :Lazy"
echo "  3. Open tmux and press prefix+I"
echo "  4. Set up SSH keys for GitHub"
chmod +x ~/.local/bin/bootstrap.sh
dot add ~/.local/bin/bootstrap.sh
dot commit -m "feat: add bootstrap script"
dot push

// part 6

Quick Reference

Essential Commands

System

sudo pacman -Syu            # full system update
paru -S <pkg>               # install from repos or AUR
paru -Rns <pkg>             # remove package and orphan deps
paru -Qs <pkg>              # search installed packages
paccache -r                 # clean cache (keep last 3 versions)
journalctl -u <svc> -f     # follow service logs
journalctl -p err -b        # all errors since last boot

Maintenance Schedule

FrequencyTask
Weeklysudo pacman -Syu — update system
Weeklypaccache -r — clean package cache
Monthlyparu -Qdtq | paru -Rns - — remove orphaned packages
MonthlyReview journalctl -p err -b — check for errors
Monthlydot pull && dot status — sync dotfiles
On changedot add, dot commit, dot push — save config changes

Neovim Quick Reference

KeyAction
Spacefffind files (Telescope)
Spacefggrep project
Spaceefile tree (Neo-tree)
SpaceggLazyGit
gdgo to definition
Khover docs
Spacernrename symbol
[d / ]dprev / next diagnostic

"system should just work" philosophy.
The best system is the one you understand completely.

Next to Setup desktop →