Ubuntu setup
These are the steps I generally take when setting up new Ubuntu machines of various types.
I previously used Ansible to automate everything, but I don't do it often enough to be worth maintaining a playbook. Plus this way is more flexible.
Table of Contents
Basic setup
WSL setup
❌ Desktop ❌ Live VM ❌ Dev VM ✔️ WSL
See WSL Setup.
User account
❌ Desktop ✔️ Live VM ✔️ Dev VM ❌ WSL
With dotfiles already installed (e.g. under root
or ubuntu
):
create-dave-user
OR manually (as root
):
useradd -c 'Dave James Miller' -G adm,sudo -s /bin/bash -mU dave
# If SSH passwords are enabled:
ssh-copy-id dave@localhost
# If not, do it manually:
umask 077
mkdir ~dave/.ssh
ssh-add -L >> ~dave/.ssh/authorized_keys
chown dave:dave ~dave/.ssh ~dave/.ssh/authorized_keys
Dotfiles
❌ Desktop ✔️ Live VM ✔️ Dev VM ✔️ WSL
Install dotfiles:
wget djm.me/dot
. dot
# May need to do this instead if `dot` is installed... Perhaps 'dot' wasn't the best name for that reason...
. ./dot
If it is a work machine, override my name and email address:
git config -f .gitconfig_local user.name 'Dave Miller'
git config -f .gitconfig_local user.email 'my.work@email.com'
Repeat for the root user if needed (sudo -i
).
Sudo
✔️ Desktop ✔️ Live VM ✔️ Dev VM ✔️ WSL
sudoedit /etc/sudoers.d/dave
# Always set $HOME so Vim writes temp files to /root/.vim/ instead of (e.g.) /home/dave/.vim/
Defaults always_set_home
Locales
✔️ Desktop ✔️ Live VM ✔️ Dev VM ✔️ WSL
sudo locale-gen en_GB.UTF-8 en_US.UTF-8
Timezone
❌ Desktop ✔️ Live VM ✔️ Dev VM ❌ WSL
sudo timedatectl set-timezone Europe/London
Hostname
❌ Desktop ✔️ Live VM ✔️ Dev VM ❌ WSL
sudo -i
hostnamectl set-hostname example.djm.me
vim /etc/hosts
Update the 127.0.1.1
line - e.g.
127.0.1.1 example.djm.me example
If Postfix has already been configured:
echo example.djm.me > /etc/mailname
Then exit back to the normal user (Ctrl-D
).
Swap
❌ Desktop ✔️ Live VM ✔️ Dev VM ❌ WSL
Check if swap is enabled already:
swapon -s
If not, create a swap file:
sudo dd if=/dev/zero of=/swap.img bs=1MiB count=2048 # 2GiB
sudo chmod 600 /swap.img
sudo mkswap /swap.img
sudo swapon /swap.img
echo '/swap.img none swap sw 0 0' | sudo tee -a /etc/fstab >/dev/null
If I need to disable it again (which is safe to do - it will drain it first):
sudo sed -i.bak '/^\/swap\.img\b/d' /etc/fstab
sudo swapoff /swap.img
sudo rm -f /swap.img
Upgrade
✔️ Desktop ✔️ Live VM ✔️ Dev VM ✔️ WSL
sudo apt update
sudo apt full-upgrade --auto-remove
# Optional, and not on WSL:
reboot
WSL symlinks
❌ Desktop ❌ Live VM ❌ Dev VM ✔️ WSL
ln -s $(wslpath "$(powershell.exe -Command "[Environment]::GetFolderPath('MyDocuments')" | tr -d '\r')") Documents
ln -s $(wslpath "$(powershell.exe -Command 'gc $env:localappdata\Dropbox\host.db | select -index 1' | base64 -di)") Dropbox
Utilities
✔️ Desktop ✔️ Live VM ✔️ Dev VM ✔️ WSL
sudo apt install bat fzf httpie pv tree vim-gtk
Note: vim-gtk
adds X11 clipboard support - but it takes much longer to install :(
Homebrew
✔️ Desktop ✔️ Live VM ✔️ Dev VM ✔️ WSL
Required for Lazy Docker, Lazy Git, Delta.
bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
reload
Git utilities
✔️ Desktop ✔️ Live VM ✔️ Dev VM ✔️ WSL
sudo apt install gcc
brew install git-delta jesseduffield/lazygit/lazygit
Certbot
❌ Desktop ✔️ Live VM ✔️ Dev VM ❌ WSL
sudo apt install snapd
sudo snap install --classic certbot
I typically use DNS validation with wildcard certificates, because it is the only way to get a certificate for local development servers. I use Cloudflare for my DNS hosting, because it is free and Certbot has first-party support for it.
sudo snap set certbot trust-plugin-with-root=ok
sudo snap install certbot-dns-cloudflare
sudo vim /etc/letsencrypt/cloudflare-credentials.ini
Create a Cloudflare API token with Zone:DNS:Edit
permission for the relevant zone(s).'
dns_cloudflare_api_token = <token>
sudo chmod 600 /etc/letsencrypt/cloudflare-credentials.ini
Create a renewal hook to restart services automatically when needed (since we'll be using certonly
):
sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
sudo vim /etc/letsencrypt/renewal-hooks/deploy/dave.sh
#!/usr/bin/env bash
set -o errexit -o nounset -o pipefail
if [[ ${RENEWED_LINEAGE:-} != '/etc/letsencrypt/live/default' ]]; then
exit
fi
if systemctl is-active -q apache2; then
echo 'Reloading Apache...'
systemctl reload apache2
systemctl status apache2
fi
if systemctl is-active -q postfix; then
echo 'Reloading Postfix...'
systemctl reload postfix
systemctl status postfix
fi
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/dave.sh
Generate a wildcard certificate:
sudo certbot certonly \
--non-interactive \
--agree-tos \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare-credentials.ini \
--dns-cloudflare-propagation-seconds 30 \
--email 'my.personal@email.com' \
--cert-name default \
--domains "$HOSTNAME,*.$HOSTNAME"
To generate certificates for additional subdomains, I usually run sudo certbot
interactively and use HTTP validation.
Postfix (outgoing emails)
❌ Desktop ✔️ Live VM ✔️ Dev VM ❌ WSL
I generally send emails through Amazon SES. For the volume I send, the cost is negligible.
sudo apt install postfix mailutils mutt
- Select "Internet Site"
- Confirm the system mail name
sudo vim /etc/postfix/main.cf
# Update:
smtpd_tls_cert_file=/etc/letsencrypt/live/default/fullchain.pem
smtpd_tls_key_file=/etc/letsencrypt/live/default/privkey.pem
# Update:
relayhost = [email-smtp.eu-west-1.amazonaws.com]:587
# Add:
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
Run:
sudo vim /etc/postfix/sasl_passwd
Either get SMTP credentials from SES (username format <hostname>-ses-postfix-user
), change the relayhost
(above and below), or remove it if not required.
[email-smtp.eu-west-1.amazonaws.com]:587 <USERNAME>:<PASSWORD>
Run:
sudo chmod 600 /etc/postfix/sasl_passwd
sudo postmap /etc/postfix/sasl_passwd
sudo vim /etc/aliases
e.g.
root: my.personal@email.com
sudo newaliases
sudo systemctl reload postfix
echo test | mail -s $HOSTNAME root
tail /var/log/mail.log
Servers
Firewall
❌ Desktop ✔️ Live VM ❌ Dev VM ❌ WSL
sudo apt install fail2ban ufw
sudo ufw default reject incoming # Default is 'deny'
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
sudo ufw enable
sudo ufw status verbose
Local directory
❌ Desktop ✔️ Live VM ✔️ Dev VM ❌ WSL
I use /local/
as the base directory (1) to make it easy to back up everything that is unique to a machine, (2) to keep my files separate from OS files, and (3) because it's what we do at work. ↩
Make a directory to hold websites & databases,11 and make sure I can write to it:
sudo mkdir /local
sudo chgrp adm /local
sudo chmod g+ws /local
Samba
❌ Desktop ❌ Live VM ✔️ Dev VM ❌ WSL
sudo apt install samba
sudo vim /etc/samba/smb.conf
Add:
[homes]
browseable = no
read only = no
[local]
path = /local
read only = no
create mask = 664
directory mask = 775
Run:
sudo systemctl reload smbd
sudo smbpasswd -a $USER
MariaDB
❌ Desktop ✔️ Live VM ✔️ Dev VM ❌ WSL
Move the files to the /local
directory:
sudo mkdir /local/mysql
sudo chown mysql:mysql /local/mysql
sudo ln -s /local/mysql /var/lib/mysql
Install and configure MariaDB:
sudo apt install mariadb-client mariadb-server
sudo mysql_secure_installation
Apache
❌ Desktop ✔️ Live VM ✔️ Dev VM ❌ WSL
Install and enable various things:
sudo apt install apache2 apache2-utils
sudo a2disconf other-vhosts-access-log
sudo a2dismod mpm_prefork
sudo a2enmod headers http2 mpm_event rewrite ssl
Tweak the settings a bit:
sudo vim /etc/apache2/conf-enabled/z_dave.conf
# Hide Apache version
ServerSignature Off
ServerTokens Prod
# Default SSL certificate
SSLCertificateFile /etc/letsencrypt/live/default/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/default/privkey.pem
#===============================================================================
# SSL hardening
#===============================================================================
# To check currently installed versions:
# apache2 -V
# openssl version
# <VirtualHost> section removed because it is handled by Ubuntu/Certbot
#===============================================================================
# generated 2023-02-02, Mozilla Guideline v5.6, Apache 2.4.41, OpenSSL 3.0.2, intermediate configuration, no HSTS
# https://ssl-config.mozilla.org/#server=apache&version=2.4.41&config=intermediate&openssl=3.0.2&hsts=false&guideline=5.6
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
SSLHonorCipherOrder off
SSLSessionTickets off
SSLUseStapling On
SSLStaplingCache "shmcb:logs/ssl_stapling(32768)"
Configure the default sites (HTTP redirects to HTTPS, and HTTPS returns a 404 page):
sudo a2dissite 000-default
sudo vim /etc/apache2/sites-enabled/001-default.conf
<VirtualHost *:80>
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/
RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R=301,L]
</VirtualHost>
<VirtualHost *:443>
SSLEngine On
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/
RewriteRule ^ [R=404,L]
</VirtualHost>
Restart Apache:
sudo systemctl restart apache2
Make site config files writable without sudo
(potentially less secure!):
sudo chgrp -R adm /etc/apache2/sites-enabled/
sudo chmod g+s /etc/apache2/sites-enabled/
sudo chmod -R ug+rwX /etc/apache2/sites-enabled/
Temp site (optional)
❌ Desktop ❌ Live VM ✔️ Dev VM ❌ WSL
Make a folder for testing small scripts, and put a phpinfo() file in it:
mkdir -p /local/temp/public
echo 'Options +Indexes' > /local/temp/public/.htaccess
echo '<?php phpinfo();' > /local/temp/public/phpinfo.php
vim /etc/apache2/sites-enabled/temp.conf
<VirtualHost *:443>
ServerName temp.example.djm.me
DocumentRoot /local/temp/public
</VirtualHost>
<Directory /local/temp/public>
AllowOverride All
Require all granted
</Directory>
sudo systemctl reload apache2
sudo systemctl status apache2
PHP
❌ Desktop ✔️ Live VM ✔️ Dev VM ✔️ WSL
Add the PPA repository:
sudo add-apt-repository -y ppa:ondrej/php
Install a given version:
version=8.2
sudo apt install php$version-{bcmath,cli,curl,dev,gd,intl,mbstring,mysql,opcache,readline,xml}
# For websites:
sudo apt install php$version-fpm
# For development:
sudo apt install php$version-xdebug
Configure it:
sudo vim /etc/php/$version/mods-available/dave.ini
For production servers, enter:
; priority=99
assert.exception = 0
display_errors = off
display_startup_errors = off
error_reporting = E_ALL
log_errors = on
zend.assertions = -1
OR for development:
; priority=99
; Error handling
assert.exception = 1
display_errors = on
display_startup_errors = on
error_reporting = E_ALL
log_errors = on
zend.assertions = 1
; Caching
opcache.revalidate_freq = 0
user_ini.cache_ttl = 0
; Xdebug - https://xdebug.org/docs/all_settings
xdebug.cli_color = 1
xdebug.max_nesting_level = 256
xdebug.mode = debug,develop
xdebug.output_dir = /tmp/xdebug
xdebug.profiler_output_name = cachegrind.out.%u
xdebug.show_local_vars = 1
Then run:
sudo phpenmod dave
sudo systemctl reload php$version-fpm
sudo systemctl status php$version-fpm
Set the default PHP version (if required):
sudo update-alternatives --set php /usr/bin/php$version
sudo update-alternatives --set php-config /usr/bin/php-config$version
#sudo update-alternatives --set phpdbg /usr/bin/phpdbg$version
sudo update-alternatives --set phpize /usr/bin/phpize$version
# For websites:
sudo update-alternatives --set php-fpm.sock /run/php/php$version-fpm.sock
sudo a2enmod proxy_fcgi
sudo a2disconf php*-fpm
sudo a2enconf php$version-fpm
sudo systemctl reload apache2
sudo systemctl status apache2
Composer
curl -sS https://getcomposer.org/installer | sudo php -- --install-dir=/usr/local/bin --filename=composer
sudo vim /etc/cron.daily/upgrade-composer
Enter:
#!/usr/bin/env bash
set -o errexit -o nounset -o pipefail
exec php /usr/local/bin/composer self-update -q
Run:
sudo chmod +x /etc/cron.daily/upgrade-composer
PsySH
composer global require psy/psysh:@stable
Docker
❌ Desktop ✔️ Live VM ✔️ Dev VM ❌ WSL
sudo add-apt-repository universe
sudo apt install docker.io docker-compose
brew install dive jesseduffield/lazydocker/lazydocker
Make Docker usable without sudo
(less secure!):
sudo gpasswd -a "$USER" docker
exec sudo su -l "$USER"
GUI Programs
PhpStorm
✔️ Desktop ❌ Live VM ✔️ Dev VM ❌ WSL
sudo apt install default-jre fonts-firacode libgbm-dev libgdm-dev openjfx
sudo snap install --classic phpstorm
DBeaver
✔️ Desktop ❌ Live VM ❌ Dev VM ❌ WSL
sudo snap install dbeaver-ce
Headless VM
Disable XDG user dirs
❌ Desktop ❌ Live VM ✔️ Dev VM ❌ WSL
xdg-user-dirs-update --set DESKTOP $HOME
xdg-user-dirs-update --set DOWNLOAD $HOME
xdg-user-dirs-update --set TEMPLATES $HOME
xdg-user-dirs-update --set PUBLICSHARE $HOME
xdg-user-dirs-update --set DOCUMENTS $HOME
xdg-user-dirs-update --set MUSIC $HOME
xdg-user-dirs-update --set PICTURES $HOME
xdg-user-dirs-update --set VIDEOS $HOME
rmdir $HOME/Desktop
rmdir $HOME/Downloads
rmdir $HOME/Templates
rmdir $HOME/Public
rmdir $HOME/Documents
rmdir $HOME/Music
rmdir $HOME/Pictures
rmdir $HOME/Videos
Desktop
✔️ Desktop ❌ Live VM ❌ Dev VM ❌ WSL
- Move the launcher bar to the bottom and size to 16px
- Configure other keys
- Install Tweaks - remap Caps Lock, disable suspend when lid closed
- Install Grub Customizer and tweak settings (if dual booting)
- Install Dropbox (enable selective sync if needed)
- Install KeePassXC
- Install Authy
- Configure Firefox
- Set a background image and colour scheme
- Install VLC ?
Remap Ctrl + Alt + arrow keys
✔️ Desktop ❌ Live VM ❌ Dev VM ❌ WSL
Works on base Ubuntu, but not on Ubuntu MATE. (11 Feb 2023)
gsettings set org.gnome.desktop.wm.keybindings 'switch-to-workspace-left' "['<Super>Page_Up', '<Super><Alt>Left']"
gsettings set org.gnome.desktop.wm.keybindings 'switch-to-workspace-right' "['<Super>Page_Down', '<Super><Alt>Right']"
gsettings set org.gnome.desktop.wm.keybindings 'switch-to-workspace-up' "['<Super><Alt>Up']"
gsettings set org.gnome.desktop.wm.keybindings 'switch-to-workspace-down' "['<Super><Alt>Down']"
gsettings set org.gnome.desktop.wm.keybindings 'move-to-workspace-left' "['<Super><Shift>Page_Up', '<Super><Shift><Alt>Left']"
gsettings set org.gnome.desktop.wm.keybindings 'move-to-workspace-right' "['<Super><Shift>Page_Down', '<Super><Shift><Alt>Right']"
gsettings set org.gnome.desktop.wm.keybindings 'move-to-workspace-up' "['<Super><Shift><Alt>Up']"
gsettings set org.gnome.desktop.wm.keybindings 'move-to-workspace-down' "['<Super><Shift><Alt>Down']"
To show the current value, use gsettings get org.gnome.desktop.wm.keybindings <action>
.
To revert, use gsettings reset org.gnome.desktop.wm.keybindings <action>
.