Create an Incus container

1. Create the container

Let’s create an ubuntu container, named test1.

incus launch images:ubuntu/noble test1 \
    -c security.nesting=true \
    -c security.syscalls.intercept.mknod=true \
    -c security.syscalls.intercept.setxattr=true

incus list
incus list -c ns4t
incus info test1
incus config show test1

The configuration -c security.nesting=true is needed in order to run docker inside the container. However if the container is unprivileged, it does not really have any security implications.

Similarly, the other two configuration options are needed so that docker can handle images efficiently.

For a Debian container use a debian image, for example images:debian/12. The rest of the instructions will be almost the same.

2. Customize the container

Let’s make a few small improvements inside the container.

  • Get a shell in the container:

    incus shell test1
  • Update/upgrade:

    apt update
    apt upgrade --yes
  • Uncomment some aliases (in debian):

    sed -i ~/.bashrc \
        -e 's/# export LS_OPTIONS=/export LS_OPTIONS=/' \
        -e 's/# alias ls=/alias ls=/' \
        -e 's/# alias ll=/alias ll=/' \
        -e 's/# alias l=/alias l=/'
    cat ~/.bashrc
  • Set a better prompt:

    sed -i ~/.bashrc -e '/bashrc_custom/d'
    echo 'source ~/.bashrc_custom' >> ~/.bashrc
    cat <<'EOF' > ~/.bashrc_custom
    # set a better prompt
    PS1='${debian_chroot:+($debian_chroot)}\[\033[01;31m\]\u\[\033[01;33m\]@\[\033[01;36m\]\h \[\033[01;33m\]\w \[\033[01;35m\]\$ \[\033[00m\]'
    EOF
  • Make sure that bash-completion is enabled:

    apt install --yes bash-completion
    
    cat <<'EOF' >> ~/.bashrc_custom
    # enable programmable completion features
    if [ -f /etc/bash_completion ] && ! shopt -oq posix; then
        source /etc/bash_completion
    fi
    EOF
    source ~/.bashrc
  • Make sure that vim is installed and enable its dark background setting:

    apt install --yes vim
    sed -i /etc/vim/vimrc \
        -e 's/^"set background=dark/set background=dark/'
  • Enable automatic security updates:

    apt install --yes unattended-upgrades

3. Set a fixed IP

Most of the containers need to have a fixed IP, so that ports can be forwarded to them from the host, etc. It is done differently on Debian and Ubuntu.

3.1. In Debian

IP=10.25.177.206/24
GW=10.25.177.1

cat <<EOF > /etc/systemd/network/eth0.network
[Match]
Name=eth0

[Address]
Address=$IP

[Route]
Gateway=$GW

[Network]
DHCP=no
DNS=8.8.8.8
EOF

cat /etc/systemd/network/eth0.network
systemctl restart systemd-networkd

ip addr
ip ro
ping google.com

3.2. In Ubuntu

apt purge cloud-init
rm /etc/netplan/50-cloud-init.yaml

IP=10.25.177.206/24
GW=10.25.177.1
cat <<EOF > /etc/netplan/01-netcfg.yaml
network:
  version: 2
  ethernets:
    eth0:
      dhcp4: no
      addresses:
        - $IP
      nameservers:
        addresses: [8.8.8.8, 8.8.4.4]
      routes:
        - to: default
          via: $GW
EOF

chmod 600 /etc/netplan/01-netcfg.yaml
cat /etc/netplan/01-netcfg.yaml

netplan apply

ip address
ip route
ping 8.8.8.8

4. Install Docker

Not all the containers need to have Docker installed, but most of them do.

  1. Add Docker’s official GPG key:

    apt install --yes ca-certificates curl
    install -m 0755 -d /etc/apt/keyrings
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
        -o /etc/apt/keyrings/docker.asc
    chmod a+r /etc/apt/keyrings/docker.asc
    ls -l /etc/apt/keyrings/docker.asc
  2. Add the repository to the apt sources:

    arch=$(dpkg --print-architecture)
    key=/etc/apt/keyrings/docker.asc
    codename=$(. /etc/os-release && echo "$VERSION_CODENAME")
    os=$(. /etc/os-release && echo "$ID")
    repo_url="https://download.docker.com/linux/$os"
    
    echo \
        "deb [arch=$arch signed-by=$key] $repo_url $codename stable" \
        > /etc/apt/sources.list.d/docker.list
    cat /etc/apt/sources.list.d/docker.list
    
    apt update
  3. Install the Docker packages

    apt install --yes \
        docker-ce \
        docker-ce-cli \
        containerd.io \
        docker-buildx-plugin \
        docker-compose-plugin
    
    docker --version
    docker compose version
    
    docker run hello-world

5. Install docker-scripts

Not all the containers need to have docker-scripts installed.

  1. First make sure that the dependencies are installed:

    apt install git make m4 highlight tree
  2. Then get the code from GitLab:

    git clone \
        https://gitlab.com/docker-scripts/ds \
        /opt/docker-scripts/ds
  3. Then install it:

    cd /opt/docker-scripts/ds/
    make install
  4. Finally check that it works:

    ds
    ds -h
    man ds

6. Script

We can use a script to automate the steps above.

Script: incus/provide-container.sh
#!/bin/bash -x

usage() {
    cat <<EOF
Usage: $0 <name> <distro> [<fixed-ip>]

    <name> is the name of the container
    <distro> is either 'ubuntu' or 'debian'

EOF
}

name=$1
distro=${2:-ubuntu}
fixed_ip=$3

[[ -z $name ]] && usage && exit 1

case $distro in
    ubuntu)
        image=images:ubuntu/noble
        ;;
    debian)
        image=images:debian/12
        ;;
    *)  
        usage
        exit 1
        ;;
esac    

### create the container
incus launch $image $name \
    -c security.nesting=true \
    -c security.syscalls.intercept.mknod=true \
    -c security.syscalls.intercept.setxattr=true
   
### create configuration script
cat <<'__EOF__' > /tmp/$name-config.sh
#!/bin/bash -x

# update and upgrade
export DEBIAN_FRONTEND=noninteractive
apt update
apt upgrade --yes

# enable automatic security updates
apt install --yes unattended-upgrades

# customize bashrc
echo 'source ~/.bashrc_custom' >> ~/.bashrc

cat <<'EOF' > ~/.bashrc_custom
# set a better prompt
PS1='${debian_chroot:+($debian_chroot)}\[\033[01;36m\]\u\[\033[01;33m\]@\[\033[01;35m\]\h \[\033[01;33m\]\w \[\033[01;31m\]\$ \[\033[00m\]'

# enable programmable completion features
if [ -f /etc/bash_completion ] && ! shopt -oq posix; then
    source /etc/bash_completion
fi
EOF

apt install --yes bash-completion

# install vim
apt install --yes vim
sed -i /etc/vim/vimrc \
    -e 's/^"set background=dark/set background=dark/'

__EOF__

if [[ $distro == 'debian' ]]; then
    cat <<'__EOF__' >> /tmp/$name-config.sh

# uncoment some aliases
sed -i ~/.bashrc \
    -e 's/# export LS_OPTIONS=/export LS_OPTIONS=/' \
    -e 's/# alias ls=/alias ls=/' \
    -e 's/# alias ll=/alias ll=/' \
    -e 's/# alias l=/alias l=/'
   
__EOF__
fi

if [[ -n $fixed_ip ]] && [[ $distro == 'debian' ]]; then
    gateway=${fixed_ip%.*}.1
    cat <<__EOF__ >> /tmp/$name-config.sh
    
cat <<EOF > /etc/systemd/network/eth0.network
[Match]
Name=eth0

[Address]
Address=$fixed_ip

[Route]
Gateway=$gateway

[Network]
DHCP=no
DNS=8.8.8.8
EOF

systemctl restart systemd-networkd

__EOF__
fi

if [[ -n $fixed_ip ]] && [[ $distro == 'ubuntu' ]]; then
    gateway=${fixed_ip%.*}.1
    cat <<__EOF__ >> /tmp/$name-config.sh
    
apt purge --yes cloud-init
rm /etc/netplan/*.yaml

cat <<EOF > /etc/netplan/01-netcfg.yaml
network:
  version: 2
  ethernets:
    eth0:
      dhcp4: no
      addresses:
        - $fixed_ip
      nameservers:
        addresses: [8.8.8.8, 8.8.4.4]
      routes:
        - to: default
          via: $gateway
EOF

chmod 600 /etc/netplan/01-netcfg.yaml
netplan apply

__EOF__
fi

### install docker and docker-scripts
cat <<'__EOF__' >> /tmp/$name-config.sh
apt install --yes ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
    -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc

arch=$(dpkg --print-architecture)
key=/etc/apt/keyrings/docker.asc
codename=$(. /etc/os-release && echo "$VERSION_CODENAME")
os=$(. /etc/os-release && echo "$ID")
repo_url="https://download.docker.com/linux/$os"

echo \
    "deb [arch=$arch signed-by=$key] $repo_url $codename stable" \
    > /etc/apt/sources.list.d/docker.list
cat /etc/apt/sources.list.d/docker.list

apt update
apt install --yes \
    docker-ce \
    docker-ce-cli \
    containerd.io \
    docker-buildx-plugin \
    docker-compose-plugin

apt install --yes \
    git make m4 highlight tree
git clone \
    https://gitlab.com/docker-scripts/ds \
    /opt/docker-scripts/ds
cd /opt/docker-scripts/ds/
make install

__EOF__

### execute
chmod +x /tmp/$name-config.sh
incus file push /tmp/$name-config.sh $name/tmp/
incus exec $name -- /tmp/$name-config.sh

### clean up
rm /tmp/$name-config.sh

### restart
incus restart $name