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
Is "umo.ci ... OCI template" not already a solution for docker/OCI > LXC "conversion"?

What are the problems with this tools-chain?
 
Is "umo.ci ... OCI template" not already a solution for docker/OCI > LXC "conversion"?

What are the problems with this tools-chain?

Not an expert but from my experimentation I can propose the following answer (and everyone is invited to correct me if I am mistaken): I think the issue with the current tools is that they convert the filesystem of a Docker image to a filesystem that we use as an LXC template. But this is not sufficient to run the image successfully. Docker's runtime allows a filesystem image to lack certain parts that won't allow the same filesystem image to run as an LXC image. This is why you see in the example instructions I linked that I had to:

- Layer the Docker image filesystem on top of a base LXC image (offering these missing components)
- Adjust the final image while simply following what is in the Docker file (mostly expected user names)
- Finally, LXC containers do not inherit the entrypoint of a Docker image simply by copying the files, this has to be re-implemented within the host base LXC image using the appropriate method (for example, if you layered the image on top of a Linux distribution powered by systemd then you would likely want to convert the docker entrypoint script into a systemd service by creating and enabling the unit file)

So for these reasons converting seems to mean more than just creating a tarball and this is where the process of truly converting remains partially a manual process.
 
Last edited:
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.

It seems my link above was incorrect - the link to github - https://github.com/berdon/docker2lxc/blob/master/docker2lxc.sh
 
  • Like
Reactions: orwadira
@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.

Yeah, this is interesting and tracks with how PVE doesn't respect docker-isms - and it really shouldn't since it isn't a docker engine. As hinted at by others above, the conversion from a docker image to a PVE usable container file is not enough - you need to manually setup the runtime operations such as entrypoint / cmd, users, etc.
 
  • Like
Reactions: orwadira