Proxmox part 4: Containers

Updated November 22, 2023 7 minutes

Here is an automated script to install Proxmox containers (LXC) from base templates, configuring their SSH servers with passwords disabled, and optionally installing Docker for nesting containers.

Honestly, LXC is cool, but KVM is compelling. If you’re in a hurry, you might want to skip directly to the next post part 5: KVM

LXC?

According to the LXC Introduction page:

LXC is a userspace interface for the Linux kernel containment
features. Through a powerful API and simple tools, it lets Linux
users easily create and manage system or application containers.

So LXC is another way to create containers on Linux, and it predates both Docker and Podman. Unlike Docker containers, LXC containers are more stateful: inside of an LXC container you usually run systemd, you can SSH into one, and you use the package manager inside to install software, and basically treat the system like a virtual machine (more like a “pet”, less like “cattle”).

Unlike virtual machines (eg. KVM, VMWare, VirtualBox), containers don’t have any virtualized hardware: instead they run as a process directly on the host system, under the same Linux kernel. You also can’t use a normal Linux distribution’s .iso file to install an LXC container, because LXC can’t “boot” a second kernel (it can only spawn PID 1, eg. systemd). Startup time is extremely quick.

In Proxmox, you install LXC containers via maintained templates. The script outlined in this post is simply automation for the process of downloading the template and creating a container based upon it.

This script currently supports the following container distributions (more are easily added):

  • arch - Arch Linux
  • debian - Debian 11
  • alpine - Alpine 3.15
  • fedora - Fedora 35

Usage

Login to your Proxmox server as the root user via SSH.

Download the script:

wget https://raw.githubusercontent.com/EnigmaCurry/blog.rymcg.tech/master/src/proxmox/proxmox_container.sh

Read all of the comments, and then edit the variables at the top of the script to change any defaults you desire. You can also override the configuration defaults in your parent shell environment.

Make the script executable:

chmod a+x proxmox_container.sh

Now run the script, passing any configuration you like to override:

DISTRO=debian \
INSTALL_DOCKER=yes \
CONTAINER_ID=100 \
CONTAINER_HOSTNAME=foo \
./proxmox_container.sh create

(The above example shows setting some variables outside the script, modifying the defaults. You can also just edit the script instead of providing them on the command line.)

The container will be created, and at the very end the IP address of the new container will be printed. You can login via SSH to the root user.

Password authentication is disabled; you must use SSH public key authentication. The default configuration will use the same SSH keys as you used for the root Proxmox user (/root/.ssh/authorized_keys).

The script

#!/bin/bash
## Automated script to setup LXC containers on Proxmox (PVE)
## See https://blog.rymcg.tech/blog/proxmox/04-containers/

## Choose your distribution base template:
## Supported: arch, debian, alpine, fedora
DISTRO=${DISTRO:-arch}

## Set these variables to configure the container:
## (All variables can be overriden from the parent environment)
CONTAINER_ID=${CONTAINER_ID:-8001}
CONTAINER_HOSTNAME=${CONTAINER_HOSTNAME:-$(echo ${DISTRO} | cut -d- -f1)}
# Container CPUs:
NUM_CORES=${NUM_CORES:-1}
# Container RAM in MB:
MEMORY=${MEMORY:-2048}
# Container swap size in MB:
SWAP_SIZE=${SWAP_SIZE:-${MEMORY}}
# Container root filesystem size in GB:
FILESYSTEM_SIZE=${FILESYSTEM_SIZE:-50}
## Point to the local authorized_keys file to copy into container:
SSH_KEYS=${SSH_KEYS:-${HOME}/.ssh/authorized_keys}
## Set an IP address or use DHCP by default:
IP_ADDRESS=${IP_ADDRESS:-dhcp}
## To install docker inside the container, set INSTALL_DOCKER=yes
INSTALL_DOCKER=${INSTALL_DOCKER:-no}

## Arch Linux specific:
ARCH_MIRROR=${ARCH_MIRROR:-"https://mirror.rackspace.com"}

## Proxmox specific variables:
PUBLIC_BRIDGE=${PUBLIC_BRIDGE:-vmbr0}
TEMPLATE_STORAGE=${TEMPLATE_STORAGE:-local}
CONTAINER_STORAGE=${CONTAINER_STORAGE:-local-lvm}

## Set YES=yes to disable all confirmations:
YES=${YES:-no}

## You can provide a password, or leave it blank to generate a secure one:
PASSWORD=${PASSWORD}
if [[ ${#PASSWORD} == 0 ]]; then
    if ! command -v openssl &> /dev/null; then
        echo "openssl is not installed. Cannot generate random password."
        exit 1
    fi
    ## Generate a long random password with openssl:
    PASSWORD=$(openssl rand -hex 45)
fi

_run() {
    pct exec ${CONTAINER_ID} -- "${@}"
}

_confirm() {
    test ${YES:-no} == "yes" && return 0
    default=$1; prompt=$2; question=${3:-". Proceed?"}
    if [[ $default == "y" || $default == "yes" ]]; then
        dflt="Y/n"
    else
        dflt="y/N"
    fi
    read -p "${prompt}${question} (${dflt}): " answer
    answer=${answer:-${default}}
    if [[ ${answer,,} == "y" || ${answer,,} == "yes" ]]; then
        return 0
    else
        echo "Exiting."
        [[ "$0" = "$BASH_SOURCE" ]] && exit 1 || return 1
    fi
}

create() {
    set -e
    ## De-Reference common short distro aliases to the longer name:
    if [[ ${DISTRO} == "arch" ]] || [[ ${DISTRO} == "archlinux" ]]; then
        DISTRO="archlinux-base"
    elif [[ ${DISTRO} == "debian" ]] || [[ ${DISTRO} == "debian-11" ]]; then
        DISTRO="debian-11-standard"
    elif [[ ${DISTRO} == "alpine" ]] || [[ ${DISTRO} == "alpine-3" ]]; then
        DISTRO="alpine-3.15"
    elif [[ ${DISTRO} == "fedora" ]]; then
        DISTRO="fedora-35"
    fi
    ## Only support specific templates that have been tested to work:
    if [[ ${DISTRO} == "archlinux-base" ]]; then
        echo "Creating Arch Linux container"
    elif [[ ${DISTRO} == "debian-11-standard" ]]; then
        echo "Creating Debian 11 container"
    elif [[ ${DISTRO} == "alpine-3.15" ]]; then
        echo "Creating Alpine 3.15 container"
    elif [[ ${DISTRO} == "fedora-35" ]]; then
        echo "Creating Fedora 35 container"
    else
        echo "DISTRO '${DISTRO}' is not supported by this script yet."
        exit 1
    fi

    test -f ${SSH_KEYS} || \
        (echo "Missing required SSH authorized_keys file: ${SSH_KEYS}" && exit 1)

    ## Download latest template
    echo "Updating templates ... "
    pveam update
    TEMPLATE=$(pveam available --section system | grep ${DISTRO} | sort -n | \
                       head -1 | tr -s ' ' | cut -d" " -f2)
    pveam download ${TEMPLATE_STORAGE} ${TEMPLATE}

    read -r -d '' CREATE_COMMAND <<EOM || true
    pct create ${CONTAINER_ID}
    ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}
    --storage ${CONTAINER_STORAGE}
    --rootfs ${CONTAINER_STORAGE}:${FILESYSTEM_SIZE}
    --unprivileged 1
    --cores ${NUM_CORES}
    --features nesting=1,keyctl=1,fuse=1
    --hostname ${CONTAINER_HOSTNAME}
    --memory ${MEMORY}
    --password ${PASSWORD}
    --net0 name=eth0,bridge=${PUBLIC_BRIDGE},firewall=1,ip=${IP_ADDRESS}
    --swap ${SWAP_SIZE}
    --ssh-public-keys ${SSH_KEYS}
EOM

    echo ""
    echo "${CREATE_COMMAND}"
    echo ""
    _confirm yes "^^ This will create the container using the above settings"
    set -x
    ${CREATE_COMMAND}

    pct start ${CONTAINER_ID}
    sleep 5
    set +x

    if [[ "${DISTRO}" =~ ^arch ]]; then
        _archlinux_init
    elif [[ "${DISTRO}" =~ ^debian ]]; then
        _debian_init
    elif [[ "${DISTRO}" =~ ^alpine ]]; then
        _alpine_init
    elif [[ "${DISTRO}" =~ ^fedora ]]; then
        _fedora_init
    fi

    set +x
    echo
    echo "Container IP address (eth0):"
    _run sh -c "ip addr show dev eth0 | grep inet"
}

_debian_init() {
    # Mask these services because they fail:
    _run systemctl mask systemd-journald-audit.socket
    _run systemctl mask sys-kernel-config.mount
    _run env apt-get update
    _run env DEBIAN_FRONTEND=noninteractive \
        apt-get \
        -o Dpkg::Options::="--force-confnew" \
        -fuy \
        dist-upgrade

    _ssh_config
    _run systemctl enable --now ssh

    if [[ ${INSTALL_DOCKER} == "yes" ]]; then
        _run apt-get -y install \
            ca-certificates \
            curl \
            gnupg \
            lsb-release
        _run sh -c "curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg"
        _run sh -c "echo \"deb [arch=\$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian \$(lsb_release -cs) stable\" | tee /etc/apt/sources.list.d/docker.list > /dev/null"
        _run apt-get update
        _run apt-get -y install docker-ce docker-ce-cli containerd.io docker-compose-plugin
    fi
}

_archlinux_init() {
    _run pacman-key --init
    _run pacman-key --populate
    _run /bin/sh -c "echo 'Server = ${ARCH_MIRROR}/archlinux/\$repo/os/\$arch' > /etc/pacman.d/mirrorlist"
    _run pacman -Syu --noconfirm

    # Mask this service because its failing:
    _run systemctl mask systemd-journald-audit.socket

    _ssh_config
    _run systemctl enable --now sshd

    if [[ ${INSTALL_DOCKER} == "yes" ]]; then
        _run pacman -S --noconfirm docker
        _run systemctl enable --now docker
    fi

}

_alpine_init() {
    _run apk upgrade -U
    _run apk add openssh
    _ssh_config
    _run rc-update add sshd
    _run /etc/init.d/sshd start

    if [[ ${INSTALL_DOCKER} == "yes" ]]; then
        _run apk add docker
        _run rc-update add docker
        _run /etc/init.d/docker start
    fi
}

_fedora_init() {
    _run dnf -y upgrade --refresh
    _run dnf -y install openssh-server less
    _ssh_config
    _run systemctl enable --now sshd

    if [[ ${INSTALL_DOCKER} == "yes" ]]; then
        _run dnf -y install dnf-plugins-core
        _run dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo
        _run dnf -y install docker-ce docker-ce-cli containerd.io
        _run systemctl enable --now docker
    fi
}

_ssh_config() {
    SSHD_CONFIG=$(mktemp)
    cat <<EOM > ${SSHD_CONFIG}
PermitRootLogin prohibit-password
PasswordAuthentication no
ChallengeResponseAuthentication no
AcceptEnv LANG LC_*
Subsystem sftp /usr/lib/openssh/sftp-server
EOM
    pct push ${CONTAINER_ID} ${SSHD_CONFIG} /etc/ssh/sshd_config
}

destroy() {
    _confirm yes "This will destroy container ${CONTAINER_ID} ($(pct config ${CONTAINER_ID} | grep hostname))"
    set -x
    pct stop "${CONTAINER_ID}" || true
    pct destroy "${CONTAINER_ID}"
}

login() {
    pct enter ${CONTAINER_ID}
}

if [[ $# == 0 ]]; then
    echo "# Documentation: https://blog.rymcg.tech/blog/proxmox/04-containers/"
    echo "Commands:"
    echo " create"
    echo " destroy"
    echo " login"
    exit 1
elif [[ $# > 1 ]]; then
    shift
    echo "Invalid arguments: $@"
    exit 1
else
    "$@"
fi


You can discuss this blog on Matrix (Element): #blog-rymcg-tech:enigmacurry.com

This blog is copyright EnigmaCurry and dual-licensed CC-BY-SA and MIT. The source is on github: enigmacurry/blog.rymcg.tech and PRs are welcome. ❤️