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.

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

❌ 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
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

Source

Desktop

✔️ Desktop ❌ Live VM ❌ Dev VM ❌ WSL

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>.


  1. 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