├── example ├── rootfs │ └── usr │ │ └── local │ │ └── bin │ │ └── hello ├── packages ├── repositories └── configure.sh ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .editorconfig ├── LICENSE ├── Makefile ├── README.adoc └── alpine-make-vm-image /example/rootfs/usr/local/bin/hello: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo 'Hello, world!' 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: jirutka 4 | -------------------------------------------------------------------------------- /example/packages: -------------------------------------------------------------------------------- 1 | chrony 2 | doas 3 | doas-sudo-shim 4 | less 5 | logrotate 6 | openssh 7 | ssmtp 8 | -------------------------------------------------------------------------------- /example/repositories: -------------------------------------------------------------------------------- 1 | @edge http://dl-cdn.alpinelinux.org/alpine/edge/main 2 | http://dl-cdn.alpinelinux.org/alpine/latest-stable/main 3 | http://dl-cdn.alpinelinux.org/alpine/latest-stable/community 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 4 8 | indent_style = tab 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.{adoc,yml}] 13 | indent_size = 2 14 | indent_style = space 15 | -------------------------------------------------------------------------------- /example/configure.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | _step_counter=0 4 | step() { 5 | _step_counter=$(( _step_counter + 1 )) 6 | printf '\n\033[1;36m%d) %s\033[0m\n' $_step_counter "$@" >&2 # bold cyan 7 | } 8 | 9 | uname -a 10 | 11 | step 'Set up timezone' 12 | setup-timezone -z Europe/Prague 13 | 14 | step 'Set up networking' 15 | cat > /etc/network/interfaces <<-EOF 16 | iface lo inet loopback 17 | iface eth0 inet dhcp 18 | EOF 19 | ln -s networking /etc/init.d/net.lo 20 | ln -s networking /etc/init.d/net.eth0 21 | 22 | step 'Adjust rc.conf' 23 | sed -Ei \ 24 | -e 's/^[# ](rc_depend_strict)=.*/\1=NO/' \ 25 | -e 's/^[# ](rc_logger)=.*/\1=YES/' \ 26 | -e 's/^[# ](unicode)=.*/\1=YES/' \ 27 | /etc/rc.conf 28 | 29 | step 'Enable services' 30 | rc-update add acpid default 31 | rc-update add chronyd default 32 | rc-update add crond default 33 | rc-update add net.eth0 default 34 | rc-update add net.lo boot 35 | rc-update add termencoding boot 36 | 37 | step 'List /usr/local/bin' 38 | ls -la /usr/local/bin 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright 2017-present Jakub Jirutka . 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SCRIPT_NAME := alpine-make-vm-image 2 | 3 | DESTDIR := / 4 | PREFIX := /usr/local 5 | 6 | SED := sed 7 | SHA1SUM := sha1sum 8 | 9 | ifeq ($(shell uname -s),Darwin) 10 | SED := gsed 11 | SHA1SUM := shasum -a 1 12 | endif 13 | 14 | #: Update version in the script and README.adoc to $VERSION. 15 | bump-version: 16 | test -n "$(VERSION)" # $$VERSION 17 | $(SED) -E -i "s/^(readonly VERSION)=.*/\1='$(VERSION)'/" $(SCRIPT_NAME) 18 | $(SED) -E -i "s/^(:version:).*/\1 $(VERSION)/" README.adoc 19 | 20 | #: Install the script into $DESTDIR. 21 | install: 22 | mkdir -p $(DESTDIR)$(PREFIX)/bin 23 | install -m 755 $(SCRIPT_NAME) $(DESTDIR)$(PREFIX)/bin/$(SCRIPT_NAME) 24 | 25 | #: Update variable :script-sha1: in README.adoc with SHA1 checksum of the script. 26 | readme-update-checksum: 27 | $(SED) -E -i \ 28 | -e "s/^(:script-sha1:).*/\1 $(shell $(SHA1SUM) $(SCRIPT_NAME) | cut -d ' ' -f 1)/" \ 29 | README.adoc 30 | 31 | #: Bump version to $VERSION, create release commit and tag. 32 | release: .check-git-clean | bump-version readme-update-checksum 33 | test -n "$(VERSION)" # $$VERSION 34 | git add . 35 | git commit -m "Release version $(VERSION)" 36 | git tag -s v$(VERSION) -m v$(VERSION) 37 | 38 | #: Print list of targets. 39 | help: 40 | @printf '%s\n\n' 'List of targets:' 41 | @$(SED) -En '/^#:.*/{ N; s/^#: (.*)\n([A-Za-z0-9_-]+).*/\2 \1/p }' $(MAKEFILE_LIST) \ 42 | | while read label desc; do printf '%-30s %s\n' "$$label" "$$desc"; done 43 | 44 | .check-git-clean: 45 | @test -z "$(shell git status --porcelain)" \ 46 | || { echo 'You have uncommitted changes!' >&2; exit 1; } 47 | 48 | .PHONY: bump-version install readme-update-checksum release help .check-git-clean 49 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - pull_request 4 | - push 5 | 6 | jobs: 7 | test-ubuntu: 8 | name: Test on Ubuntu 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Install qemu-utils 12 | run: | 13 | sudo apt-get update 14 | sudo apt-get install qemu-utils 15 | 16 | - uses: actions/checkout@v4 17 | 18 | - name: Build image for x86_64 with BIOS mode and without GPT 19 | run: | 20 | sudo ./alpine-make-vm-image \ 21 | --image-format qcow2 \ 22 | --image-size 2G \ 23 | --repositories-file example/repositories \ 24 | --packages "$(cat example/packages)" \ 25 | --fs-skel-dir example/rootfs \ 26 | --fs-skel-chown root:root \ 27 | --script-chroot \ 28 | alpine-bios-$(date +%Y-%m-%d).qcow2 -- ./example/configure.sh 29 | 30 | - name: Build image for x86_64 with BIOS mode and GPT 31 | run: | 32 | sudo ./alpine-make-vm-image \ 33 | --image-format raw \ 34 | --image-size 2G \ 35 | --partition \ 36 | --repositories-file example/repositories \ 37 | --packages "$(cat example/packages)" \ 38 | --fs-skel-dir example/rootfs \ 39 | --fs-skel-chown root:root \ 40 | --script-chroot \ 41 | alpine-bios-part-$(date +%Y-%m-%d).raw -- ./example/configure.sh 42 | 43 | - name: Build image for x86_64 with UEFI mode 44 | run: | 45 | sudo ./alpine-make-vm-image \ 46 | --image-format qcow2 \ 47 | --image-size 2G \ 48 | --boot-mode UEFI \ 49 | --repositories-file example/repositories \ 50 | --packages "$(cat example/packages)" \ 51 | --fs-skel-dir example/rootfs \ 52 | --fs-skel-chown root:root \ 53 | --script-chroot \ 54 | alpine-uefi-$(date +%Y-%m-%d).qcow2 -- ./example/configure.sh 55 | 56 | - name: Install qemu-aarch64 and register in binfmt 57 | uses: jirutka/setup-alpine@v1 58 | with: 59 | arch: aarch64 60 | 61 | - name: Build image for aarch64 62 | run: | 63 | sudo ./alpine-make-vm-image \ 64 | --arch aarch64 \ 65 | --image-format qcow2 \ 66 | --image-size 2G \ 67 | --repositories-file example/repositories \ 68 | --packages "$(cat example/packages) linux-virt@edge" \ 69 | --fs-skel-dir example/rootfs \ 70 | --fs-skel-chown root:root \ 71 | --script-chroot \ 72 | alpine-aarch64-$(date +%Y-%m-%d).qcow2 -- ./example/configure.sh 73 | 74 | test-alpine: 75 | name: Test on Alpine 76 | runs-on: ubuntu-latest 77 | steps: 78 | - uses: actions/checkout@v3 79 | 80 | # We must run this outside the chroot. 81 | - run: sudo modprobe nbd max_part=16 82 | 83 | - name: Set up Alpine Linux v3.22 84 | uses: jirutka/setup-alpine@v1 85 | with: 86 | # XXX: v3.23 doesn't work here, running package triggers results in "unshare: Invalid argument" 87 | branch: v3.22 88 | 89 | - name: Build image for x86_64 with BIOS mode and without GPT 90 | run: | 91 | ./alpine-make-vm-image \ 92 | --image-format qcow2 \ 93 | --image-size 2G \ 94 | --repositories-file example/repositories \ 95 | --packages "$(cat example/packages)" \ 96 | --fs-skel-dir example/rootfs \ 97 | --fs-skel-chown root:root \ 98 | --script-chroot \ 99 | alpine-bios-$(date +%Y-%m-%d).qcow2 -- ./example/configure.sh 100 | shell: alpine.sh --root {0} 101 | 102 | - name: Build image for x86_64 with UEFI mode 103 | run: | 104 | ./alpine-make-vm-image \ 105 | --image-format qcow2 \ 106 | --image-size 2G \ 107 | --boot-mode UEFI \ 108 | --repositories-file example/repositories \ 109 | --packages "$(cat example/packages)" \ 110 | --fs-skel-dir example/rootfs \ 111 | --fs-skel-chown root:root \ 112 | --script-chroot \ 113 | alpine-uefi-$(date +%Y-%m-%d).qcow2 -- ./example/configure.sh 114 | shell: alpine.sh --root {0} 115 | 116 | - name: Install qemu-aarch64 and register in binfmt 117 | uses: jirutka/setup-alpine@v1 118 | with: 119 | arch: aarch64 120 | shell-name: alpine-aarch64.sh 121 | 122 | # Note: We cannot run alpine-make-vm-image inside emulated chroot due to nbd. 123 | - name: Build image for aarch64 124 | run: | 125 | ./alpine-make-vm-image \ 126 | --arch aarch64 \ 127 | --branch edge \ 128 | --image-format qcow2 \ 129 | --image-size 2G \ 130 | --packages "$(cat example/packages)" \ 131 | --fs-skel-dir example/rootfs \ 132 | --fs-skel-chown root:root \ 133 | --script-chroot \ 134 | alpine-aarch64-$(date +%Y-%m-%d).qcow2 -- ./example/configure.sh 135 | shell: alpine.sh --root {0} 136 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = Make Alpine Linux VM Image 2 | :script-name: alpine-make-vm-image 3 | :script-sha1: f17ef4997496ace524a8e8e578d944f3552255bb 4 | :gh-name: alpinelinux/{script-name} 5 | :version: 0.13.3 6 | 7 | ifdef::env-github[] 8 | image:https://github.com/{gh-name}/workflows/CI/badge.svg["Build Status", link="https://github.com/{gh-name}/actions"] 9 | endif::env-github[] 10 | 11 | This project provides a script for making customized https://alpinelinux.org/[Alpine Linux] disk images for x86_64 and aarch64 footnote:[Supported since Alpine Linux v3.19. See <>.] virtual machines. 12 | You can choose between BIOS mode (using https://syslinux.org/[Syslinux], only for x86_64) and UEFI mode (using Linux https://docs.kernel.org/admin-guide/efi-stub.html[EFI stub]). 13 | It’s quite simple (400 LoC of shell), fast (~32 seconds on GitHub Actions), requires minimum dependencies (QEMU and filesystem tools). 14 | 15 | TIP: Don’t need VM, just wanna chroot into Alpine Linux? 16 | Try https://github.com/alpinelinux/alpine-chroot-install[alpine-chroot-install]! 17 | Or do you want to create a custom rootfs? 18 | Then https://github.com/alpinelinux/alpine-make-rootfs[alpine-make-rootfs] is for you! 19 | 20 | 21 | == Requirements 22 | 23 | * Linux system with common userland (Busybox or GNU coreutils) 24 | * POSIX-sh compatible shell (e.g. Busybox ash, dash, Bash, ZSH) 25 | * `qemu-img` and `qemu-nbd` tools 26 | * `rsync` (needed only for `--fs-skel-dir`) 27 | * `sfdisk` (needed only for `--partition`, `--boot-mode UEFI` and non-x86 architectures) 28 | * `mdev` or `udevadm` (needed only for `--partition`, `--boot-mode UEFI` and non-x86 architectures if device hotplug doesn’t work) 29 | * `e2fsprogs` (for ext4), `btrfs-progs` (for Btrfs), or `xfsprogs` (for XFS) 30 | * `dosfstools` (needed only for `--boot-mode UEFI` and non-x86 architectures) 31 | 32 | All dependencies except the first two are automatically installed by the script when running on Alpine Linux. 33 | 34 | 35 | == Usage 36 | 37 | Read documentation in link:{script-name}[{script-name}]. 38 | See link:.github/workflows/ci.yml[] for GitHub Actions example. 39 | 40 | You can copy link:{script-name}[{script-name}] into your repository or download it on demand, e.g.: 41 | 42 | [source, sh, subs="+attributes"] 43 | wget https://raw.githubusercontent.com/{gh-name}/v{version}/{script-name} \ 44 | && echo '{script-sha1} {script-name}' | sha1sum -c \ 45 | || exit 1 46 | 47 | Or, if you are on Alpine Linux, you can simply install the https://pkgs.alpinelinux.org/packages?name={script-name}[{script-name}] package. 48 | 49 | 50 | == Howtos 51 | 52 | === Create images for aarch64 on x86_64 host 53 | 54 | All you need to do is install the https://www.qemu.org/docs/master/user/main.html[QEMU User space emulator] for aarch64 and register it in https://docs.kernel.org/admin-guide/binfmt-misc.html[binfmt_misc] as the interpreter for aarch64 binaries. 55 | 56 | On Alpine Linux:: 57 | + 58 | [source, sh] 59 | apk add qemu-aarch64 qemu-openrc 60 | rc-service qemu-binfmt start 61 | 62 | On Debian/Ubuntu:: 63 | + 64 | [source, sh] 65 | apt-get install -y --no-install-recommends binfmt-support qemu-user-static 66 | update-binfmts --enable 67 | 68 | On Fedora:: 69 | + 70 | [source, sh] 71 | dnf install qemu-user-static 72 | 73 | On GitHub Actions:: 74 | + 75 | [source, yaml] 76 | ---- 77 | - name: Install qemu-aarch64 and register in binfmt 78 | uses: jirutka/setup-alpine@v1 79 | with: 80 | arch: aarch64 81 | ---- 82 | + 83 | See link:.github/workflows/ci.yml[] for a complete example. 84 | 85 | After that, run {script-name} with the option `--arch aarch64`. 86 | 87 | 88 | [[aarch64-old]] 89 | === Create aarch64 image with Alpine v3.18 or older 90 | 91 | The Linux kernel (_linux-virt_, _linux-lts_ or _linux-edge_ package) in Alpine v3.18 and earlier doesn’t have https://cateee.net/lkddb/web-lkddb/EFI_ZBOOT.html[EFI_ZBOOT] enabled, so EFI stub cannot load a compressed vmlinuz. 92 | We backported it to v3.18, but then we had to revert it due to a problem with Grub (see https://gitlab.alpinelinux.org/alpine/aports/-/issues/15263[alpine/aports#15263]). 93 | 94 | If you want to build an image with an older branch of Alpine Linux, you can, but you must install the kernel from the v3.19 branch (or newer). 95 | This is relatively safe because the kernel package doesn’t have any dynamic dependencies. 96 | 97 | . Create a `repositories` file with a pinned main repository from v3.19, e.g.: 98 | + 99 | [source] 100 | ---- 101 | @v319 https://dl-cdn.alpinelinux.org/alpine/v3.19/main 102 | https://dl-cdn.alpinelinux.org/alpine/v3.18/main 103 | https://dl-cdn.alpinelinux.org/alpine/v3.18/community 104 | ---- 105 | 106 | . Run {script-name} with the options `--repositories-file ./repositories` and `--packages linux-virt@v319` (or `linux-lts@v319` if you use `--kernel-flavor lts`). 107 | 108 | This will first install _linux-virt_ from v3.18, but in the later step it will reinstall it from the v3.19 branch. 109 | 110 | 111 | === Create image for VMware (ESXi) 112 | 113 | VMware and disk images (virtual disks) is one big mess. 114 | You can find that VMware uses the VMDK format, but the problem is that this is not a single format. 115 | Actually it has many subformats with very different structure and various (in)compatibility with VMware hypervisors. 116 | 117 | When I’ve created a disk image using `qemu-img create -f vmdk` or converted Qcow2 to VMDK using `qemu-img convert -O vmdk`, vSphere client loaded this image without any problem, but the data was corrupted. 118 | Eventually I found in some old documentation that ESXi does not support “sparse” disks… 119 | 120 | So after many trials I found out that the least bad and functional solution is to create Qcow2 image and then convert it to VMDK using: 121 | 122 | [source, sh] 123 | qemu-img convert -f qcow2 -O vmdk -o adapter_type=lsilogic,subformat=monolithicFlat alpine.qcow2 alpine.vmdk 124 | 125 | Unfortunately, this creates a “thick” image, i.e. its size equals the “provisioned space”, not actually used space as in Qcow2. 126 | However, you can compress it with gzip to avoid transferring multiple gigabytes of zeros over network. 127 | 128 | 129 | == License 130 | 131 | This project is licensed under http://opensource.org/licenses/MIT/[MIT License]. 132 | For the full text of the license, see the link:LICENSE[LICENSE] file. 133 | -------------------------------------------------------------------------------- /alpine-make-vm-image: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # vim: set ts=4 sw=4: 3 | # SPDX-FileCopyrightText: © 2017 Jakub Jirutka 4 | # SPDX-License-Identifier: MIT 5 | #---help--- 6 | # Usage: alpine-make-vm-image [options] [--] [