Implement rootless Docker container with enhanced security and preserved TFTP logging (#87)

* Implement rootless Docker container with preserved TFTP logging

- Convert all processes to run as non-root user (nbxyz) for enhanced security
- Add customizable PUID/PGID environment variables for volume permission management
- Implement privilege dropping using gosu for secure initialization
- Optimize Dockerfile with multi-stage build and better caching
- Create dnsmasq wrapper script to ensure TFTP logs appear in docker logs
- Configure supervisord to properly forward dnsmasq output to container stdout
- Maintain full TFTP/PXE boot debugging functionality for users

Security improvements:
- All application processes (nginx, webapp, dnsmasq) run as nbxyz user
- Only supervisord initialization runs as root, then drops privileges
- Proper permission management for all service directories
- Configurable user/group IDs via PUID/PGID environment variables

Performance optimizations:
- Multi-stage Docker build reduces final image size
- Better layer caching for faster rebuilds
- Optimized package installation and cleanup

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Update Dockerfile

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update root/init.sh

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Antony Messerli 2025-06-12 00:19:27 -05:00 committed by GitHub
parent 994dc608a9
commit ca7a3390c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 203 additions and 51 deletions

59
.dockerignore Normal file
View File

@ -0,0 +1,59 @@
# Git and version control
.git
.gitignore
.gitattributes
# Documentation
README.md
*.md
docs/
# CI/CD
.github/
.gitlab-ci.yml
.travis.yml
# Docker files
Dockerfile*
docker-compose*
.dockerignore
# Node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Logs
*.log
logs/
# Temporary files
tmp/
temp/
.tmp
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Testing
coverage/
.nyc_output/
test-results/
# Build artifacts
dist/
build/

View File

@ -1,69 +1,106 @@
# Build stage - Download and prepare webapp
FROM alpine:3.22.0 AS build
# set version label
# Set version label
ARG WEBAPP_VERSION
RUN apk add --no-cache \
# Install build dependencies with virtual package for easy cleanup
RUN apk add --no-cache --virtual .build-deps \
bash \
busybox \
curl \
git \
jq \
npm && \
mkdir /app && \
if [ -z ${WEBAPP_VERSION+x} ]; then \
npm \
&& mkdir /app \
# Determine webapp version if not provided
&& if [ -z "${WEBAPP_VERSION+x}" ]; then \
WEBAPP_VERSION=$(curl -sX GET "https://api.github.com/repos/netbootxyz/webapp/releases/latest" \
| awk '/tag_name/{print $4;exit}' FS='[""]'); \
fi && \
curl -o /tmp/webapp.tar.gz -L \
"https://github.com/netbootxyz/webapp/archive/${WEBAPP_VERSION}.tar.gz" && \
tar xf /tmp/webapp.tar.gz -C /app/ --strip-components=1 && \
npm install --prefix /app && \
rm -rf /tmp/*
fi \
# Download and extract webapp
&& curl -o /tmp/webapp.tar.gz -L \
"https://github.com/netbootxyz/webapp/archive/${WEBAPP_VERSION}.tar.gz" \
&& tar xf /tmp/webapp.tar.gz -C /app/ --strip-components=1 \
# Install only production dependencies
&& cd /app \
&& npm install --omit=dev --no-audit --no-fund \
# Clean up build artifacts and cache
&& npm cache clean --force \
&& rm -rf /tmp/* \
&& apk del .build-deps
# Production stage - Final container
FROM alpine:3.22.0
# set version label
# Build arguments for labels
ARG BUILD_DATE
ARG VERSION
ARG VCS_REF
LABEL build_version="netboot.xyz version: ${VERSION} Build-date: ${BUILD_DATE}"
LABEL maintainer="antonym"
LABEL org.opencontainers.image.description="netboot.xyz official docker container - Your favorite operating systems in one place. A network-based bootable operating system installer based on iPXE."
# Enhanced container labels following OCI spec
LABEL org.opencontainers.image.title="netboot.xyz" \
org.opencontainers.image.description="Your favorite operating systems in one place. A network-based bootable operating system installer based on iPXE." \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.revision="${VCS_REF}" \
org.opencontainers.image.vendor="netboot.xyz" \
org.opencontainers.image.url="https://netboot.xyz" \
org.opencontainers.image.source="https://github.com/netbootxyz/docker-netbootxyz" \
org.opencontainers.image.licenses="Apache-2.0" \
maintainer="antonym"
# Install runtime dependencies and configure system in a single layer
RUN apk add --no-cache \
# Core utilities
bash \
busybox \
curl \
dnsmasq \
envsubst \
git \
jq \
nghttp2-dev \
tar \
# Network services
dnsmasq \
nginx \
nodejs \
# System services
shadow \
sudo \
supervisor \
syslog-ng \
tar && \
groupmod -g 1000 users && \
useradd -u 911 -U -d /config -s /bin/false nbxyz && \
usermod -G users nbxyz && \
mkdir /app /config /defaults
# Security tools
gosu \
# Runtime libraries
nghttp2-dev \
# Create required directories
&& mkdir -p /app /config /defaults \
# Remove unnecessary packages to reduce size
&& rm -rf /var/cache/apk/*
# Copy webapp from build stage
COPY --from=build /app /app
ENV TFTPD_OPTS=''
ENV NGINX_PORT='80'
ENV WEB_APP_PORT='3000'
# Environment variables with defaults
ENV TFTPD_OPTS='' \
NGINX_PORT='80' \
WEB_APP_PORT='3000' \
NODE_ENV='production' \
NPM_CONFIG_CACHE='/tmp/.npm' \
PUID='1000' \
PGID='1000'
EXPOSE 69/udp
EXPOSE 80
EXPOSE 3000
COPY root/ /
# Copy configuration files and scripts
COPY --chown=root:root root/ /
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 CMD /healthcheck.sh
# Make scripts executable
RUN chmod +x /start.sh /init.sh /healthcheck.sh /usr/local/bin/dnsmasq-wrapper.sh
CMD ["sh","/start.sh"]
# Enhanced health check with better timing for slow systems
HEALTHCHECK --interval=30s --timeout=15s --start-period=60s --retries=3 \
CMD /healthcheck.sh
# Use exec form for better signal handling
CMD ["/start.sh"]

View File

@ -57,6 +57,8 @@ The following snippets are examples of starting up the container.
```shell
docker run -d \
--name=netbootxyz \
-e PUID=1000 `# optional, UserID for volume permissions` \
-e PGID=1000 `# optional, GroupID for volume permissions` \
-e MENU_VERSION=2.0.84 `# optional` \
-e NGINX_PORT=80 `# optional` \
-e WEB_APP_PORT=3000 `# optional` \
@ -114,6 +116,8 @@ Container images are configured using parameters passed at runtime (such as thos
| `-p 3000` | Web configuration interface. |
| `-p 69/udp` | TFTP Port. |
| `-p 80` | NGINX server for hosting assets. |
| `-e PUID=1000` | UserID for volume permissions - see below for explanation |
| `-e PGID=1000` | GroupID for volume permissions - see below for explanation |
| `-e WEB_APP_PORT=3000` | Specify a different port for the web configuration interface to listen on. |
| `-e NGINX_PORT=80` | Specify a different port for NGINX service to listen on. |
| `-e MENU_VERSION=2.0.76` | Specify a specific version of boot files you want to use from netboot.xyz (unset pulls latest) |
@ -121,6 +125,23 @@ Container images are configured using parameters passed at runtime (such as thos
| `-v /config` | Storage for boot menu files and web application config |
| `-v /assets` | Storage for netboot.xyz bootable assets (live CDs and other files) |
## User / Group Identifiers
When using volumes (`-v` flags), permissions issues can arise between the host OS and the container. We avoid this issue by allowing you to specify the user `PUID` and group `PGID`.
Ensure any volume directories on the host are owned by the same user you specify and any permissions issues will vanish like magic.
In this instance `PUID=1000` and `PGID=1000`, to find yours use `id your_user` as below:
```bash
id your_user
```
Example output:
```bash
uid=1000(your_user) gid=1000(your_user) groups=1000(your_user)
```
## DHCP Configurations
The netboot.xyz Docker image requires the usage of a DHCP server in order to function properly. If you have an existing DHCP server, usually you will need to make some small adjustments to make your DHCP server forward requests to the netboot.xyz container. The main settings in your DHCP or router that you will typically need to set are:

View File

@ -1,33 +1,29 @@
[supervisord]
nodaemon=true
user=root
[program:syslog-ng]
command=/usr/sbin/syslog-ng --foreground --no-caps
stdout_syslog=true
stdout_capture_maxbytes=1MB
priority = 1
silent=false
logfile=/tmp/supervisord.log
pidfile=/run/supervisord.pid
[program:nginx]
command = /usr/sbin/nginx -c /config/nginx/nginx.conf
command = gosu nbxyz /usr/sbin/nginx -c /config/nginx/nginx.conf
startretries = 2
daemon=off
priority = 2
stdout_logfile=/dev/null
stderr_logfile=/dev/null
[program:webapp]
environment=NODE_ENV="production",PORT=%(ENV_WEB_APP_PORT)s
command=/usr/bin/node app.js
user=nbxyz
command=gosu nbxyz /usr/bin/node app.js
directory=/app
priority = 3
stdout_logfile=/dev/null
stderr_logfile=/dev/null
[program:dnsmasq]
command=/usr/sbin/dnsmasq --port=0 --keep-in-foreground --enable-tftp --user=nbxyz --tftp-secure --tftp-root=/config/menus %(ENV_TFTPD_OPTS)s
stdout_logfile=/config/tftpd.log
command=/usr/local/bin/dnsmasq-wrapper.sh %(ENV_TFTPD_OPTS)s
priority = 3
redirect_stderr=true
priority = 4
[program:messages-log]
command=tail -f /var/log/messages
stdout_logfile=/dev/stdout
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0

View File

@ -1,5 +1,28 @@
#!/bin/bash
# Configure user and group IDs
PUID=${PUID:-1000}
PGID=${PGID:-1000}
echo "[init] Setting up user nbxyz with PUID=${PUID} and PGID=${PGID}"
# Create group with specified GID if it doesn't exist
if ! getent group ${PGID} > /dev/null 2>&1; then
groupadd -g ${PGID} nbxyz
else
echo "[init] Group with GID ${PGID} already exists"
fi
# Create user with specified UID if it doesn't exist
if ! getent passwd ${PUID} > /dev/null 2>&1; then
useradd -u ${PUID} -g ${PGID} -d /config -s /bin/false nbxyz
else
echo "[init] User with UID ${PUID} already exists"
fi
# Add to users group for compatibility
usermod -a -G users nbxyz 2>/dev/null || true
# make our folders
mkdir -p \
/assets \
@ -7,7 +30,8 @@ mkdir -p \
/config/log/nginx \
/run \
/var/lib/nginx/tmp/client_body \
/var/tmp/nginx
/var/tmp/nginx \
/var/log
# copy config files
[[ ! -f /config/nginx/nginx.conf ]] && \
@ -15,10 +39,12 @@ mkdir -p \
[[ ! -f /config/nginx/site-confs/default ]] && \
envsubst '${NGINX_PORT}' < /defaults/default > /config/nginx/site-confs/default
# Ownership
# Set up permissions for all directories that services need to write to
chown -R nbxyz:nbxyz /assets
chown -R nbxyz:nbxyz /var/lib/nginx
chown -R nbxyz:nbxyz /var/log/nginx
chown -R nbxyz:nbxyz /config/log/nginx
chown -R nbxyz:nbxyz /run
chown -R nbxyz:nbxyz /var/tmp/nginx
# create local logs dir
mkdir -p \

View File

@ -1,6 +1,6 @@
#!/bin/bash
# Perform the initial configuration
# Perform the initial configuration as root
/init.sh
echo " _ _ _ "
@ -15,4 +15,7 @@ echo
echo "https://opencollective.com/netbootxyz"
echo "https://github.com/sponsors/netbootxyz"
echo
supervisord -c /etc/supervisor.conf
# Run supervisord as root (it will use gosu for individual programs)
echo "[start] Starting supervisord (programs will run as nbxyz)"
exec supervisord -c /etc/supervisor.conf

View File

@ -0,0 +1,10 @@
#!/bin/bash
# Wrapper script for dnsmasq to ensure TFTP logs are visible in docker logs
echo "[dnsmasq] Starting TFTP server on port 69"
echo "[dnsmasq] TFTP root: /config/menus"
echo "[dnsmasq] TFTP security: enabled"
echo "[dnsmasq] Logging: enabled (dhcp and queries)"
# Start dnsmasq via gosu with logging to stderr (which supervisord can capture)
exec gosu nbxyz /usr/sbin/dnsmasq --port=0 --keep-in-foreground --enable-tftp --user=nbxyz --tftp-secure --tftp-root=/config/menus --log-facility=- --log-dhcp --log-queries "$@"