Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -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/
97 changes: 67 additions & 30 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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/ /

# Make scripts executable
RUN chmod +x /start.sh /init.sh /healthcheck.sh /usr/local/bin/dnsmasq-wrapper.sh

HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 CMD /healthcheck.sh
# Enhanced health check with better timing for slow systems
HEALTHCHECK --interval=30s --timeout=15s --start-period=60s --retries=3 \
CMD /healthcheck.sh

CMD ["sh","/start.sh"]
# Use exec form for better signal handling
CMD ["/start.sh"]
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` \
Expand Down Expand Up @@ -114,13 +116,32 @@ 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) |
| `-e TFTPD_OPTS='--tftp-single-port'` | Specify arguments for the TFTP server (this example makes TFTP send all data over port 69) |
| `-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:
Expand Down
28 changes: 12 additions & 16 deletions root/etc/supervisor.conf
Original file line number Diff line number Diff line change
@@ -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
32 changes: 29 additions & 3 deletions root/init.sh
Original file line number Diff line number Diff line change
@@ -1,24 +1,50 @@
#!/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 \
/config/nginx/site-confs \
/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 ]] && \
cp /defaults/nginx.conf /config/nginx/nginx.conf
[[ ! -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 \
Expand Down
7 changes: 5 additions & 2 deletions root/start.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/bin/bash

# Perform the initial configuration
# Perform the initial configuration as root
/init.sh

echo " _ _ _ "
Expand All @@ -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
10 changes: 10 additions & 0 deletions root/usr/local/bin/dnsmasq-wrapper.sh
Original file line number Diff line number Diff line change
@@ -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 "$@"