iPXE Server

This commit is contained in:
2026-02-02 21:50:03 +01:00
commit 96249d0c06
6 changed files with 743 additions and 0 deletions

246
Containerfile Normal file
View File

@@ -0,0 +1,246 @@
# ------------------------------------------------------------------
# ██╗██████╗ ██╗ ██╗███████╗ Lightweight HTTP server
# ██║██╔══██╗╚██╗██╔╝██╔════╝ for network booting.
# ██║██████╔╝ ╚███╔╝ █████╗
# ██║██╔═══╝ ██╔██╗ ██╔══╝ Serves boot menus, kernels, and images
# ██║██║ ██╔╝ ██╗███████╗
# ╚═╝╚═╝ ╚═╝ ╚═╝╚══════╝ Version 1.0.0
# ███████╗███████╗██████╗ ██╗ ██╗███████╗██████╗
# ██╔════╝██╔════╝██╔══██╗██║ ██║██╔════╝██╔══██╗
# ███████╗█████╗ ██████╔╝██║ ██║█████╗ ██████╔╝
# ╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██╔══╝ ██╔══██╗
# ███████║███████╗██║ ██║ ╚████╔╝ ███████╗██║ ██║
# ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝
# Author: Kevin [+ Claude]
# Created: 2025-02-02
# ------------------------------------------------------------------
FROM docker.io/library/alpine:3.21 AS base
# Install runtime dependencies
RUN apk add --no-cache \
nginx \
curl \
tzdata \
tini \
&& rm -rf /var/cache/apk/*
# Create ipxe user
RUN addgroup -g 1000 ipxe && \
adduser -D -s /sbin/nologin -u 1000 -G ipxe ipxe
# Create directory structure
RUN mkdir -p \
/srv/boot \
/srv/menus \
/srv/images \
/srv/scripts \
/etc/ipxe \
/var/log/nginx \
/var/lib/nginx \
/run/nginx && \
chown -R ipxe:ipxe /srv /var/log/nginx /var/lib/nginx /run/nginx
# nginx configuration for iPXE
RUN cat > /etc/nginx/nginx.conf << 'EOF'
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /run/nginx/nginx.pid;
events {
worker_connections 256;
use epoll;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Custom MIME types for iPXE
types {
application/octet-stream ipxe efi;
text/plain ipxe menu;
}
log_format ipxe '$remote_addr - [$time_local] "$request" $status $body_bytes_sent '
'"$http_user_agent" rt=$request_time';
access_log /var/log/nginx/access.log ipxe;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
# Disable buffering for faster boot file delivery
proxy_buffering off;
server {
listen 8080;
server_name _;
root /srv;
# Boot files (kernels, initramfs, iPXE binaries)
location /boot/ {
alias /srv/boot/;
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
}
# iPXE menus and scripts
location /menus/ {
alias /srv/menus/;
autoindex on;
default_type text/plain;
}
# ISO/squashfs images
location /images/ {
alias /srv/images/;
autoindex on;
autoindex_exact_size on;
# Support range requests for large files
add_header Accept-Ranges bytes;
}
# Dynamic menu endpoint (can be extended with scripts)
location /menu {
alias /srv/menus/boot.ipxe;
default_type text/plain;
}
# Health check endpoint
location /health {
access_log off;
return 200 'OK\n';
add_header Content-Type text/plain;
}
# Status page
location /status {
stub_status on;
access_log off;
}
}
}
EOF
# Health check script
RUN cat > /usr/local/bin/healthcheck.sh << 'EOF'
#!/bin/sh
set -e
curl -sf http://localhost:8080/health > /dev/null || exit 1
echo "iPXE server healthy"
EOF
RUN chmod +x /usr/local/bin/healthcheck.sh
# Entrypoint script
RUN cat > /usr/local/bin/entrypoint.sh << 'EOF'
#!/bin/sh
set -e
echo "=== iPXE Boot Server ==="
echo "Boot files: /srv/boot"
echo "Menus: /srv/menus"
echo "Images: /srv/images"
echo ""
# List available boot assets
echo "Available boot files:"
ls -la /srv/boot/ 2>/dev/null || echo " (none)"
echo ""
echo "Available menus:"
ls -la /srv/menus/ 2>/dev/null || echo " (none)"
echo ""
# Validate nginx config
nginx -t
# Start nginx in foreground
exec nginx -g 'daemon off;'
EOF
RUN chmod +x /usr/local/bin/entrypoint.sh
# Default boot menu template
RUN cat > /srv/menus/boot.ipxe << 'EOF'
#!ipxe
# iPXE Boot Menu - NUC Home Server
# Served via Caddy reverse proxy
set menu-timeout 30000
set menu-default local
:start
menu iPXE Boot Menu - nuc.lan
item --gap -- -------- Boot Options --------
item local Boot from local disk
item --gap -- -------- Network Boot --------
item fedora-live Fedora 42 Live (Minimal)
item fedora-kiosk Fedora 42 Kiosk/PoS
item fedora-rescue Fedora 42 Rescue
item --gap -- -------- Utilities --------
item memtest Memtest86+
item shell iPXE Shell
item reboot Reboot
item exit Exit to BIOS
choose --timeout ${menu-timeout} --default ${menu-default} selected || goto cancel
goto ${selected}
:cancel
echo Boot cancelled
goto start
:local
sanboot --no-describe --drive 0x80 || goto start
:fedora-live
echo Booting Fedora 42 Live (Minimal)...
set base-url http://ipxe.nuc.lan/images/fedora-42
kernel ${base-url}/vmlinuz initrd=initrd.img root=live:${base-url}/squashfs.img rd.live.image rd.live.overlay.overlayfs=1 quiet
initrd ${base-url}/initrd.img
boot || goto start
:fedora-kiosk
echo Booting Fedora 42 Kiosk...
set base-url http://ipxe.nuc.lan/images/fedora-42-kiosk
kernel ${base-url}/vmlinuz initrd=initrd.img root=live:${base-url}/squashfs.img rd.live.image quiet
initrd ${base-url}/initrd.img
boot || goto start
:fedora-rescue
echo Booting Fedora 42 Rescue...
set base-url http://ipxe.nuc.lan/images/fedora-42
kernel ${base-url}/vmlinuz initrd=initrd.img rescue quiet
initrd ${base-url}/initrd.img
boot || goto start
:memtest
echo Loading Memtest86+...
kernel http://ipxe.nuc.lan/boot/memtest86+.bin
boot || goto start
:shell
echo Dropping to iPXE shell...
shell
:reboot
reboot
:exit
exit
EOF
EXPOSE 8080
USER ipxe
WORKDIR /srv
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["/usr/local/bin/entrypoint.sh"]
LABEL maintainer="Kevin" \
version="1.0.0" \
description="iPXE boot server for network booting"

48
README.md Normal file
View File

@@ -0,0 +1,48 @@
# IPXE Boot server
Boot from ipxe.nuc.lan:8080
## Files
| File | Purpose |
|-------------------------|-------------------------------------------------|
| `ipxe-server.container` | Quadlet unit joining `internal_caddy` network |
| `Containerfile` | Alpine + nginx serving boot assets on port 8080 |
| `snippets/ipxe` | Caddy snippet with reusable proxy directives |
| `caddy/ipxe.caddyfile` | Site block for `ipxe.nuc.lan` (HTTP + HTTPS) |
| `setup.sh` | Automated deployment script |
**Integration with existing Caddyfile:**
1. Copy `snippets/ipxe` to your snippets directory
2. Add to top of your Caddyfile:
```
import snippets/ipxe
```
3. Add the `ipxe.nuc.lan` block (or paste the content from `ipxe.caddyfile`)
**Or** since you have `*.nuc.lan` already, add this matcher to your wildcard block:
```caddy
@ipxe host ipxe.nuc.lan
handle @ipxe {
reverse_proxy ipxe-server:8080
}
```
**Quick deploy:**
```bash
./setup.sh install # Creates dirs, builds image, installs Quadlet
./setup.sh start # Starts via systemd
# Add boot files
cp vmlinuz initrd.img ~/ipxe/boot/
cp squashfs.img ~/ipxe/images/fedora-42/
```
**Note:** HTTP is intentionally kept open for `ipxe.nuc.lan:80` because most PXE ROMs chainload via HTTP before the full iPXE stack with HTTPS support is loaded. The local network restriction handles security.

114
ipxe.caddyfile Normal file
View File

@@ -0,0 +1,114 @@
# === iPXE BOOT SERVER ===
# Add this to your main Caddyfile or include via import
# Requires: import snippets/ipxe at the top of Caddyfile
# HTTPS endpoint for iPXE (local network)
ipxe.nuc.lan {
tls /etc/caddy/tls/wildcard.nuc.lan.crt /etc/caddy/tls/wildcard.nuc.lan.key {
protocols tls1.2 tls1.3
}
# Only allow local network access
@local_network {
remote_ip 192.168.1.0/24 10.89.0.0/24 10.10.5.0/24
}
# Boot menus - no caching
@menus {
path /menus/* /menu
}
handle @menus {
import ipxe-headers
import ipxe-upstream
}
# Boot files (kernels, initramfs) - cache aggressively
@boot {
path /boot/*
}
handle @boot {
import ipxe-boot-headers
import ipxe-upstream
}
# Images (ISO, squashfs) - moderate caching, range support
@images {
path /images/*
}
handle @images {
import ipxe-image-headers
import ipxe-upstream
}
# Health/status endpoints
@health {
path /health /status
}
handle @health {
import ipxe-upstream
}
# Default handler for local network
handle @local_network {
import ipxe-upstream
}
# Block external access
handle {
respond "Access denied" 403
}
log {
output file /var/log/caddy/ipxe.log {
roll_size 10MB
roll_keep 3
}
format json
}
}
# HTTP endpoint for iPXE (required for legacy/chainloading)
# iPXE firmware often starts with HTTP before HTTPS
ipxe.nuc.lan:80 {
# Allow HTTP for initial iPXE chainload (most PXE ROMs don't support HTTPS)
@local_network {
remote_ip 192.168.1.0/24 10.89.0.0/24 10.10.5.0/24
}
# Menus via HTTP (for initial boot)
@menus {
path /menus/* /menu
}
handle @menus {
import ipxe-headers
import ipxe-upstream
}
# Boot files via HTTP
@boot {
path /boot/*
}
handle @boot {
import ipxe-boot-headers
import ipxe-upstream
}
# Images via HTTP (for clients that don't support HTTPS)
@images {
path /images/*
}
handle @images {
import ipxe-image-headers
import ipxe-upstream
}
# Health endpoint
handle @local_network {
import ipxe-upstream
}
handle {
respond "Access denied" 403
}
}

51
ipxe.snippet Normal file
View File

@@ -0,0 +1,51 @@
# snippets/ipxe
# iPXE Boot Server reverse proxy configuration
# Import in Caddyfile with: import snippets/ipxe
(ipxe-headers) {
header {
# Disable caching for menus (dynamic content)
Cache-Control "no-cache, no-store, must-revalidate"
Pragma "no-cache"
-Server
}
}
(ipxe-boot-headers) {
header {
# Cache boot files aggressively (kernels, initramfs rarely change)
Cache-Control "public, max-age=86400"
-Server
}
}
(ipxe-image-headers) {
header {
# Moderate caching for images
Cache-Control "public, max-age=3600"
# Support range requests for large files
Accept-Ranges "bytes"
-Server
}
}
(ipxe-upstream) {
# Reverse proxy to iPXE container
reverse_proxy ipxe-server:8080 {
# Timeouts for large file transfers
transport http {
response_header_timeout 30s
read_timeout 300s
write_timeout 300s
}
# Health check
health_uri /health
health_interval 30s
health_timeout 5s
# Fail fast if unhealthy
fail_duration 30s
}
}

50
quadlet Normal file
View File

@@ -0,0 +1,50 @@
# ipxe-server.container
# Quadlet unit for iPXE boot server
# Place in: ~/.config/containers/systemd/ (rootless) or /etc/containers/systemd/ (root)
[Unit]
Description=iPXE Boot Server
After=network-online.target
Wants=network-online.target
Requires=caddy.service
After=caddy.service
[Container]
Image=localhost/ipxe-server:latest
ContainerName=ipxe-server
# Join caddy network for reverse proxy
Network=internal_caddy
# Mount points for boot assets and configuration
Volume=%h/ipxe/boot:/srv/boot:ro,Z
Volume=%h/ipxe/menus:/srv/menus:ro,Z
Volume=%h/ipxe/config:/etc/ipxe:ro,Z
# Optional: mount ISO storage for serving live images
Volume=%h/ipxe/images:/srv/images:ro,Z
# Environment
Environment=IPXE_LOG_LEVEL=info
Environment=TZ=Europe/Oslo
# Resource limits
PodmanArgs=--memory=256m --cpus=0.5
# Labels for Caddy/Traefik discovery (optional)
Label=ipxe.server=true
# Health check
HealthCmd=/usr/local/bin/healthcheck.sh
HealthInterval=30s
HealthTimeout=5s
HealthRetries=3
[Service]
Restart=on-failure
RestartSec=10
TimeoutStartSec=90
[Install]
WantedBy=default.target

234
setup.sh Executable file
View File

@@ -0,0 +1,234 @@
#!/bin/bash
# Setup script for iPXE boot server
# Creates directory structure and deploys Quadlet
set -euo pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[0;33m'
NC='\033[0m'
log_info() { printf "${BLUE}[INFO]${NC} %s\n" "$1"; }
log_ok() { printf "${GREEN}[OK]${NC} %s\n" "$1"; }
log_warn() { printf "${YELLOW}[WARN]${NC} %s\n" "$1"; }
log_err() { printf "${RED}[ERROR]${NC} %s\n" "$1"; }
# Configuration
IPXE_BASE="${IPXE_BASE:-$HOME/ipxe}"
QUADLET_DIR="${QUADLET_DIR:-$HOME/.config/containers/systemd}"
CADDY_SNIPPETS="${CADDY_SNIPPETS:-$HOME/Caddy/snippets}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
usage() {
cat << EOF
iPXE Boot Server Setup
Usage: $0 [command]
Commands:
install Install Quadlet and create directories
build Build the container image
start Start the iPXE server
stop Stop the iPXE server
status Show server status
logs Show server logs
uninstall Remove Quadlet unit
Options:
--rootless Install for rootless Podman (default)
--root Install system-wide (requires sudo)
EOF
}
create_directories() {
log_info "Creating directory structure..."
mkdir -p "$IPXE_BASE"/{boot,menus,images,config}
mkdir -p "$QUADLET_DIR"
mkdir -p "$CADDY_SNIPPETS"
log_ok "Directories created at $IPXE_BASE"
}
install_quadlet() {
log_info "Installing Quadlet unit..."
# Copy Quadlet file
cp "$SCRIPT_DIR/ipxe-server.container" "$QUADLET_DIR/"
# Update paths in Quadlet to use actual home directory
sed -i "s|%h/ipxe|$IPXE_BASE|g" "$QUADLET_DIR/ipxe-server.container"
log_ok "Quadlet installed to $QUADLET_DIR/ipxe-server.container"
}
install_caddy_snippet() {
log_info "Installing Caddy snippet..."
cp "$SCRIPT_DIR/snippets/ipxe" "$CADDY_SNIPPETS/"
log_ok "Caddy snippet installed to $CADDY_SNIPPETS/ipxe"
log_warn "Don't forget to:"
log_warn " 1. Add 'import snippets/ipxe' to your Caddyfile"
log_warn " 2. Add the ipxe.nuc.lan site block from caddy/ipxe.caddyfile"
log_warn " 3. Reload Caddy: podman exec caddy caddy reload --config /etc/caddy/Caddyfile"
}
copy_default_menu() {
log_info "Copying default boot menu..."
if [[ ! -f "$IPXE_BASE/menus/boot.ipxe" ]]; then
# Extract default menu from Containerfile or use embedded one
cat > "$IPXE_BASE/menus/boot.ipxe" << 'EOF'
#!ipxe
# iPXE Boot Menu - NUC Home Server
set menu-timeout 30000
set menu-default local
:start
menu iPXE Boot Menu - nuc.lan
item --gap -- -------- Boot Options --------
item local Boot from local disk
item --gap -- -------- Network Boot --------
item fedora-live Fedora 42 Live (Minimal)
item fedora-kiosk Fedora 42 Kiosk/PoS
item --gap -- -------- Utilities --------
item shell iPXE Shell
item reboot Reboot
choose --timeout ${menu-timeout} --default ${menu-default} selected || goto cancel
goto ${selected}
:cancel
echo Boot cancelled
goto start
:local
sanboot --no-describe --drive 0x80 || goto start
:fedora-live
echo Booting Fedora 42 Live...
set base-url http://ipxe.nuc.lan/images/fedora-42
kernel ${base-url}/vmlinuz initrd=initrd.img root=live:${base-url}/squashfs.img rd.live.image quiet
initrd ${base-url}/initrd.img
boot || goto start
:fedora-kiosk
echo Booting Fedora 42 Kiosk...
set base-url http://ipxe.nuc.lan/images/fedora-42-kiosk
kernel ${base-url}/vmlinuz initrd=initrd.img root=live:${base-url}/squashfs.img rd.live.image quiet
initrd ${base-url}/initrd.img
boot || goto start
:shell
shell
:reboot
reboot
EOF
log_ok "Default boot menu created"
else
log_warn "Boot menu already exists, skipping"
fi
}
build_image() {
log_info "Building iPXE server container image..."
podman build -t localhost/ipxe-server:latest -f "$SCRIPT_DIR/Containerfile" "$SCRIPT_DIR"
log_ok "Image built: localhost/ipxe-server:latest"
}
reload_systemd() {
log_info "Reloading systemd user units..."
systemctl --user daemon-reload
log_ok "Systemd reloaded"
}
start_server() {
log_info "Starting iPXE server..."
systemctl --user start ipxe-server
log_ok "iPXE server started"
}
stop_server() {
log_info "Stopping iPXE server..."
systemctl --user stop ipxe-server
log_ok "iPXE server stopped"
}
show_status() {
systemctl --user status ipxe-server
}
show_logs() {
journalctl --user -u ipxe-server -f
}
uninstall() {
log_info "Uninstalling iPXE server..."
systemctl --user stop ipxe-server 2>/dev/null || true
rm -f "$QUADLET_DIR/ipxe-server.container"
systemctl --user daemon-reload
log_ok "Quadlet removed"
log_warn "Data in $IPXE_BASE preserved. Remove manually if needed."
}
do_install() {
create_directories
install_quadlet
install_caddy_snippet
copy_default_menu
build_image
reload_systemd
echo ""
log_ok "Installation complete!"
echo ""
echo "Next steps:"
echo " 1. Add boot files to: $IPXE_BASE/boot/"
echo " 2. Add images to: $IPXE_BASE/images/"
echo " 3. Edit boot menu: $IPXE_BASE/menus/boot.ipxe"
echo " 4. Update Caddy config (see caddy/ipxe.caddyfile)"
echo " 5. Start server: $0 start"
echo ""
}
# Main
case "${1:-}" in
install)
do_install
;;
build)
build_image
;;
start)
start_server
;;
stop)
stop_server
;;
status)
show_status
;;
logs)
show_logs
;;
uninstall)
uninstall
;;
-h|--help|help|"")
usage
;;
*)
log_err "Unknown command: $1"
usage
exit 1
;;
esac