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 .
~/.dotfiles, version-controlled on GitHub.// contents
- Hardware Overview
- Create Bootable USB
- Pre-Install Network
- Disk Partitioning
- Format & Mount
- Select Mirrors
- Install Base System
- Chroot & Configure
- Bootloader
- Post-Install Config
- SSH Hardening
- Firewall (nftables)
- AUR Helper (paru)
- Shell (zsh)
- tmux
- Neovim
- Languages & Runtimes
- Git Config
- Dev Services
- Hardening
- Dotfiles Management
- Bootstrap Script
- Quick Reference
// hardware
ThinkPad T480 Overview
Hardware
| Component | T480 Spec | Notes |
|---|---|---|
| CPU | Intel Core i5/i7 8th Gen | Good Linux support, intel_pstate driver |
| RAM | Up to 64GB DDR4 (2 slots) | Max it out — ZFS or Docker loves RAM |
| NIC | Intel I219-LM | Excellent kernel support, use for server NIC |
| WiFi | Intel 8265 | iwlwifi driver, works OOTB |
| Storage | NVMe M.2 + 2.5" SATA | Use NVMe for OS, SATA for data if available |
| Display | 14" IPS | Irrelevant for a headless server |
BIOS Settings
Change these before install:
| Setting | Value |
|---|---|
| Security › Secure Boot | Disabled |
| Config › Network › Wake on LAN | Enabled |
| Startup › UEFI/Legacy Boot | UEFI Only |
| Config › Thunderbolt BIOS Assist Mode | Enabled |
// 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)
| Partition | Size | Type | Mount | Purpose |
|---|---|---|---|---|
| /dev/nvme0n1p1 | 512M | EFI System | /boot/efi | Bootloader |
| /dev/nvme0n1p2 | 4G | Linux swap | [SWAP] | Swap |
| /dev/nvme0n1p3 | Remainder | Linux 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
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
| Package | Purpose |
|---|---|
| base | Minimal Arch userspace (bash, glibc, coreutils) |
| base-devel | Build tools: gcc, make, binutils, fakeroot, sudo |
| linux | The kernel |
| linux-firmware | Firmware blobs for WiFi, Bluetooth, etc. |
| linux-headers | Required to build DKMS kernel modules |
| intel-ucode | Critical. CPU microcode updates. Prevents obscure bugs and security holes. |
| networkmanager | Network management. Main network tool. |
| openssh | Needed for SSH. |
| git | Needed for version controll and development. |
| nvim | My editor of choice. |
| man-db, man-pages, texinfo | Man 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
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
sudo nvim /etc/ssh/sshd_config
| Directive | Value | Reason |
|---|---|---|
| Port | 2222 | Non-standard port, reduces script kiddie noise |
| PermitRootLogin | no | Never allow root SSH login |
| PasswordAuthentication | no | Key-only auth |
| PubkeyAuthentication | yes | |
| MaxAuthTries | 3 | |
| X11Forwarding | no | |
| AllowTcpForwarding | no |
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 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
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
| Key | Action |
|---|---|
| C-a d | detach (session keeps running) |
| C-a c | new 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
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
| Frequency | Task |
|---|---|
| Weekly | sudo pacman -Syu — update system |
| Weekly | paccache -r — clean package cache |
| Monthly | paru -Qdtq | paru -Rns - — remove orphaned packages |
| Monthly | Review journalctl -p err -b — check for errors |
| Monthly | dot pull && dot status — sync dotfiles |
| On change | dot add, dot commit, dot push — save config changes |
Neovim Quick Reference
| Key | Action |
|---|---|
| Spaceff | find files (Telescope) |
| Spacefg | grep project |
| Spacee | file tree (Neo-tree) |
| Spacegg | LazyGit |
| gd | go to definition |
| K | hover docs |
| Spacern | rename symbol |
| [d / ]d | prev / next diagnostic |
"system should just work" philosophy.
The best system is the one you understand completely.