Docker to PVE-LXC conversion steps/tool?

I've been setting up a little homelab and wanted to be able to convert docker images to LXC containers. Given the LXC container is just a tar.gz of the rootfs, and docker images are just a tar.gz of a bunch of filesystem layers, I threw together a quick script that converts them without needing to use docker to run/create the container.

A quick note, the docker engine handles running the ENTRYPOINT/CMD commands. This won't do that so you _will_ need to update the LXC container, once running, to do that. I might add that in but it strongly depends on the linux distribution used (e.g. systemd, upstart, etc).

The script is here but I'll include it below as well.

Bash:
#!/bin/bash

# Check if image name is provided
if [ -z "$1" ]; then
  echo "Usage: $0 <image-name>[:<tag>]"
  echo "Example: $0 ubuntu:latest"
  exit 1
fi

# Assign input image name
IMAGE="$1"
# Generate output filename based on image name, replacing '/' and ':' with '_'
OUTPUT_NAME=$(echo "${IMAGE}" | sed 's/[:\/]/_/g').tar.gz
# Temporary directories
TEMP_DIR=$(mktemp -d)
EXTRACT_DIR="${TEMP_DIR}/extract"
ROOTFS_DIR="${TEMP_DIR}/rootfs"

# Check if Docker is installed and running
if ! command -v docker &> /dev/null; then
  echo "Error: Docker is not installed."
  exit 1
fi
if ! docker info &> /dev/null; then
  echo "Error: Docker daemon is not running."
  exit 1
fi

# Check if jq is installed (for parsing JSON)
if ! command -v jq &> /dev/null; then
  echo "Error: jq is required for parsing JSON. Install it with 'apt install jq' or similar."
  exit 1
fi

# Check if the image exists locally, pull if it doesn't
if ! docker image inspect "${IMAGE}" &> /dev/null; then
  echo "Pulling image ${IMAGE}..."
  docker pull "${IMAGE}" || {
    echo "Error: Failed to pull image ${IMAGE}."
    exit 1
  }
fi

echo "Saving image ${IMAGE} to tar..."
# Save the image to a tar file
IMAGE_TAR="${TEMP_DIR}/image.tar"
docker save -o "${IMAGE_TAR}" "${IMAGE}" || {
  echo "Error: Failed to save image."
  exit 1
}

echo "Extracting image tar..."
# Create extract directory and extract the tar
mkdir -p "${EXTRACT_DIR}"
tar -xf "${IMAGE_TAR}" -C "${EXTRACT_DIR}" || {
  echo "Error: Failed to extract image tar."
  rm -rf "${TEMP_DIR}"
  exit 1
}

echo "Parsing manifest.json..."
# Parse the manifest.json to get the layers (assuming single image manifest)
MANIFEST="${EXTRACT_DIR}/manifest.json"
if [ ! -f "${MANIFEST}" ]; then
  echo "Error: manifest.json not found."
  rm -rf "${TEMP_DIR}"
  exit 1
fi
LAYERS=$(jq -r '.[0].Layers[]' "${MANIFEST}")

# Create rootfs directory
mkdir -p "${ROOTFS_DIR}"

echo "Extracting and merging layers..."
# For each layer, extract and handle whiteouts
for LAYER in ${LAYERS}; do
  LAYER_TAR="${EXTRACT_DIR}/${LAYER}"
  if [ ! -f "${LAYER_TAR}" ]; then
    echo "Error: Layer tar ${LAYER} not found."
    rm -rf "${TEMP_DIR}"
    exit 1
  fi

  # Extract the layer tar to rootfs
  tar -xf "${LAYER_TAR}" -C "${ROOTFS_DIR}"

  # Handle whiteouts: find all .wh.* files
  find "${ROOTFS_DIR}" -type f -name '.wh.*' | while read -r WHITEOUT; do
    # Get the directory and the name to delete
    DIR=$(dirname "${WHITEOUT}")
    NAME=$(basename "${WHITEOUT}" | sed 's/^\.wh\.//')

    if [ "${NAME}" = ".wh..opq" ]; then
      # Opaque directory: remove all contents except whiteouts (but for flatten, we can skip or handle as delete dir)
      # For simplicity, treat as deleting the directory contents from lower layers, but since we extract in order, just remove the marker
      rm -f "${WHITEOUT}"
    else
      # Regular whiteout: remove the actual file/dir if exists
      TARGET="${DIR}/${NAME}"
      rm -rf "${TARGET}"
      # Remove the whiteout marker
      rm -f "${WHITEOUT}"
    fi
  done
done

echo "Compressing rootfs to ${OUTPUT_NAME}..."
# Create the tar.gz from rootfs
tar -czf "${OUTPUT_NAME}" -C "${ROOTFS_DIR}" . || {
  echo "Error: Failed to create ${OUTPUT_NAME}."
  rm -rf "${TEMP_DIR}"
  exit 1
}

# Clean up
echo "Cleaning up temporary files..."
rm -rf "${TEMP_DIR}"

echo "Success: Root filesystem built and exported to ${OUTPUT_NAME}"
 
Last edited:
  • Like
Reactions: orwadira and waltar
Just registered to answer to this topic, a proxmox newbie so please be somewhat kind :P .

I recently bought a machine to play with and have frigate on proxmox, little did I know that this will not be an easy ride.
Recently had some time so I did some effort.

I did not know the situation around tteck and I am sad to hear about it.

I have, however, managed finally to **update to a more recent version of frigate using bare lxc container. However, it needs quite some manual work and of 'course it is not thoroughly tested as I am currently just finishing just being able to run it.

I have multiple questions:
- Now that we have such a big loss in the community is someone somewhere forking the community scripts?
- If so, how can I contribute (probably a PR if it's on github somewhere)
- New frigate version uses quite more env vars which I am not sure how to set in LXCs. I've read a post somewhere people discussing that passing them through the config is not ideal, as it passes them to the init script which doesn't really care about env vars so it drops them(?).
- Generally, I will be happy to help anyone else trying this. I did all these as my pc has an intel n150 which needs a community made i915 driver which needs debian 12 to be build.

Edit: After posting this I understood that probably someone has forked this already in a community accepted way and see the repo already. I will see if it's possible to do a pull request when I have a more sure that it works version of my edits.
 
Last edited:
@b3bis The work seem to have been continued, there is a website now https://community-scripts.github.io/ProxmoxVE which excellently serve most of these scripts (and seems to be adding more scripts all the time). The LXC/VMs can also be updated simply by typing `update` in the shell.

My words of pragmatic wisdom when wanting to run something in Proxmox is first, to look for an LXC image of the thing you are trying to run. If failed, look for a Proxmox community installer (through the website above), if failed, simply start from a similar LXC image and install what you need, possibly using Ansible.

To be honest, converting a Docker image to an LXC running container is a painful process in my experience (maybe others have a different opinion) and can only be justified in my opinion if the Docker image was prepared by an expert and contains partially incompatible or difficult-to-exist-together components. A recent attempt that qualified for me was while I was trying to install a number of PostgreSQL extensions together in the same system, needed for an AI application, which I could not build on the system simultaneously by myself (I think some of the extensions required versions of libcurl that are different from the other extensions).

Anyway, this was one of the few times where I felt it was justified to go through the effort. To give an idea of what the conversion entailed in this case, I have uploaded an example document here: https://github.com/diraneyya/docker2lxc/blob/master/EXAMPLE.md

@auhanson you may find this interesting since you have also been experimenting with this like I have.
 
Last edited:
  • Like
Reactions: waltar