GitHub: github.com/png261
Email: nhphuong.code@gmail.com
# syntax=docker/dockerfile:1
# Build arguments for versioning and multi-arch support
ARG NODE_VERSION=20
ARG ALPINE_VERSION=3.20
ARG NGINX_VERSION=1.26.2
ARG BUILD_DATE
ARG GIT_COMMIT
################################################################################
# Stage 1: Application Builder
################################################################################
FROM node:${NODE_VERSION}-alpine@sha256:2d5e8a8a51bc341fd5f2eed6d91455c3a3d147e91a14298fc564b5dc519c1666 AS builder
WORKDIR /app
# Setup pnpm with corepack (built-in Node.js 16+)
ENV PNPM_HOME="/pnpm" \
PATH="$PNPM_HOME:$PATH"
RUN corepack enable && \
corepack prepare pnpm@9.12.2 --activate
# Install dependencies with cache mount for faster rebuilds
COPY package.json pnpm-lock.yaml .npmrc ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
pnpm install --frozen-lockfile --prefer-offline
# Copy source code and configuration files
COPY tsconfig.json tsconfig.node.json vite.config.ts ./
COPY postcss.config.js tailwind.config.ts biome.json ./
COPY index.html ./
COPY public ./public
COPY src ./src
# Build application and clean artifacts
ENV NODE_ENV=production
RUN pnpm build && \
find dist -type f \( -name "*.map" -o -name ".*" \) -delete && \
rm -f dist/stats.html
################################################################################
# Stage 2: Nginx Static Binary Builder
################################################################################
FROM alpine:${ALPINE_VERSION}@sha256:1e42bbe2508154c9126d48c2b8a75420c3544343bf86fd041fb7527e017a4b4a AS nginx-builder
ARG NGINX_VERSION
ARG TARGETPLATFORM
ARG BUILDPLATFORM
# SHA256 checksum for nginx source verification
ENV NGINX_SHA256=627fe086209bba80a2853a0add9d958d7ebbdffa1a8467a5784c9a6b4f03d738
# Log build platform info for debugging multi-arch builds
RUN echo "Building on $BUILDPLATFORM for $TARGETPLATFORM"
# Install build dependencies
RUN apk add --no-cache \
gcc g++ musl-dev make linux-headers curl \
pcre-dev pcre2-dev zlib-dev zlib-static \
openssl-dev openssl-libs-static upx
# Download and verify nginx source
WORKDIR /tmp
RUN curl -fSL "https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz" -o nginx.tar.gz && \
echo "${NGINX_SHA256} nginx.tar.gz" | sha256sum -c -
# Build fully static nginx with minimal modules
RUN tar -xzf nginx.tar.gz && \
cd "nginx-${NGINX_VERSION}" && \
./configure \
--prefix=/usr/local/nginx \
--sbin-path=/usr/local/nginx/sbin/nginx \
--conf-path=/etc/nginx/nginx.conf \
--pid-path=/run/nginx.pid \
--lock-path=/run/nginx.lock \
--error-log-path=/dev/stderr \
--http-log-path=/dev/stdout \
--user=nobody \
--group=nobody \
# Performance and essential features
--with-threads \
--with-file-aio \
--with-http_ssl_module \
--with-http_v2_module \
--with-http_gzip_static_module \
--with-http_stub_status_module \
--with-pcre \
--with-pcre-jit \
# Static linking and optimization flags
--with-cc-opt='-static -Os -ffunction-sections -fdata-sections' \
--with-ld-opt='-static -Wl,--gc-sections' \
# Disable all unnecessary modules for minimal binary
--without-http_charset_module \
--without-http_ssi_module \
--without-http_userid_module \
--without-http_auth_basic_module \
--without-http_mirror_module \
--without-http_autoindex_module \
--without-http_geo_module \
--without-http_map_module \
--without-http_split_clients_module \
--without-http_referer_module \
--without-http_rewrite_module \
--without-http_proxy_module \
--without-http_fastcgi_module \
--without-http_uwsgi_module \
--without-http_scgi_module \
--without-http_grpc_module \
--without-http_memcached_module \
--without-http_limit_conn_module \
--without-http_limit_req_module \
--without-http_empty_gif_module \
--without-http_browser_module \
--without-http_upstream_hash_module \
--without-http_upstream_ip_hash_module \
--without-http_upstream_least_conn_module \
--without-http_upstream_random_module \
--without-http_upstream_keepalive_module \
--without-http_upstream_zone_module \
--without-mail_pop3_module \
--without-mail_imap_module \
--without-mail_smtp_module \
--without-stream_limit_conn_module \
--without-stream_access_module \
--without-stream_geo_module \
--without-stream_map_module \
--without-stream_split_clients_module \
--without-stream_return_module \
--without-stream_set_module \
--without-stream_upstream_hash_module \
--without-stream_upstream_least_conn_module \
--without-stream_upstream_random_module \
--without-stream_upstream_zone_module && \
make -j"$(nproc)" && \
make install
# Optimize binary: strip debug symbols + UPX compression
RUN strip --strip-all /usr/local/nginx/sbin/nginx && \
upx --best --lzma /usr/local/nginx/sbin/nginx && \
/usr/local/nginx/sbin/nginx -V
################################################################################
# Stage 3: Asset Pre-compression
################################################################################
FROM alpine:${ALPINE_VERSION}@sha256:1e42bbe2508154c9126d48c2b8a75420c3544343bf86fd041fb7527e017a4b4a AS compressor
RUN apk add --no-cache brotli gzip findutils
WORKDIR /app
COPY --from=builder /app/dist ./dist
# Parallel compression: gzip + brotli for all text-based assets
RUN find dist -type f \
\( -name "*.html" -o -name "*.css" -o -name "*.js" -o \
-name "*.json" -o -name "*.svg" -o -name "*.xml" \) \
-print0 | xargs -0 -P"$(nproc)" -I {} sh -c 'gzip -9 -k -f "{}" && brotli -q 11 -f "{}"'
################################################################################
# Stage 4: Minimal Filesystem Preparation
################################################################################
FROM alpine:${ALPINE_VERSION}@sha256:1e42bbe2508154c9126d48c2b8a75420c3544343bf86fd041fb7527e017a4b4a AS rootfs
# Create complete directory structure
RUN mkdir -p \
/rootfs/etc/nginx/conf.d \
/rootfs/usr/share/nginx/html \
/rootfs/var/log/nginx \
/rootfs/var/cache/nginx \
/rootfs/usr/local/nginx/{client_body,proxy,fastcgi,uwsgi,scgi}_temp \
/rootfs/tmp \
/rootfs/run && \
chmod 1777 /rootfs/tmp
# Create minimal user database (nobody user)
RUN echo "nobody:x:65534:65534:nobody:/:/sbin/nologin" > /rootfs/etc/passwd && \
echo "nobody:x:65534:" > /rootfs/etc/group
# Copy nginx configuration
COPY nginx.conf /rootfs/etc/nginx/conf.d/default.conf
COPY --from=nginx-builder /etc/nginx/mime.types /rootfs/etc/nginx/mime.types
COPY --from=compressor /app/dist /rootfs/usr/share/nginx/html
# Create main nginx.conf
RUN cat > /rootfs/etc/nginx/nginx.conf <<'EOF'
worker_processes auto;
error_log stderr warn;
pid /run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /dev/stdout;
# Performance optimizations
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
server_tokens off;
# Compression
gzip on;
gzip_static on;
gzip_vary on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
include /etc/nginx/conf.d/*.conf;
}
EOF
# Set proper ownership for non-root user
RUN chown -R 65534:65534 \
/rootfs/usr/share/nginx/html \
/rootfs/var/log/nginx \
/rootfs/var/cache/nginx \
/rootfs/usr/local/nginx \
/rootfs/tmp \
/rootfs/run
################################################################################
# Stage 5: Final Distroless Image
################################################################################
FROM scratch
# Re-declare build args for metadata
ARG BUILD_DATE
ARG GIT_COMMIT=unknown
# OCI-compliant metadata labels
LABEL org.opencontainers.image.title="Vite React - Distroless" \
org.opencontainers.image.description="Distroless minimal image (<6MB) - UPX compressed" \
org.opencontainers.image.version="2.2.0-distroless-upx" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.revision="${GIT_COMMIT}" \
org.opencontainers.image.base.name="scratch" \
org.opencontainers.image.source="https://github.com/riipandi/vite-react-template" \
org.opencontainers.image.licenses="MIT OR Apache-2.0" \
maintainer="contest-2025-optimized"
# Copy static nginx binary and minimal filesystem
COPY --from=nginx-builder /usr/local/nginx/sbin/nginx /usr/sbin/nginx
COPY --from=rootfs /rootfs /
# Run as non-root user (nobody = UID 65534)
USER 65534:65534
EXPOSE 3000
# Lightweight healthcheck using nginx config test
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["/usr/sbin/nginx", "-t", "-q"]
STOPSIGNAL SIGTERM
ENTRYPOINT ["/usr/sbin/nginx"]
CMD ["-g", "daemon off;"]
| Stage | Input | Output | Size | Time |
|---|---|---|---|---|
1: Application Buildernode:20-alpine |
package.json, source code | /app/dist (optimized assets) | 450 MB (discarded) | <1s (cached) / 34s (cold) |
2: Nginx Builderalpine:3.20 |
nginx-1.26.2.tar.gz | Static nginx binary (2.1 MB) | 450 MB (discarded) | <1s (cached) / 30s (cold) |
3: Compressoralpine:3.20 |
Static assets | Pre-compressed (.gz + .br) | 150 MB (discarded) | <1s (cached) / 1.2s (cold) |
4: Rootfs Prepalpine:3.20 |
Configs, assets | Complete rootfs structure | 50 MB (discarded) | <1s (cached) |
5: FinalFROM scratch |
Nginx (2.1MB), rootfs (0.3MB) | Production image | 2.43 MB ✓ | <1s |
| FINAL RESULTS | |
|---|---|
| Image size | 2.43 MB |
| Base image | scratch (0 MB) |
| Layers | 2 data layers |
| CVEs | 0 vulnerabilities |
| Startup time | 3-5 seconds |
| Memory (idle) | 10-12 MB |
| Shutdown time | 0.212 seconds |
| HTTP response | < 10ms |
| Build time (cold) | 64 seconds |
| Build time (warm) | 0.2 seconds |
| vs nginx:alpine (7.5 MB) | 67.6% smaller |
| vs nginx:latest (192 MB) | 98.7% smaller |
Mục đích: Build ứng dụng Vite React thành static assets
FROM node:${NODE_VERSION}-alpine@sha256:... AS builder
WORKDIR /app
# Setup pnpm with corepack
ENV PNPM_HOME="/pnpm" \
PATH="$PNPM_HOME:$PATH"
RUN corepack enable && corepack prepare pnpm@9.12.2 --activate
# Install dependencies with cache mount
COPY package.json pnpm-lock.yaml .npmrc ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
pnpm install --frozen-lockfile --prefer-offline
# Copy source and build configuration
COPY tsconfig.json tsconfig.node.json vite.config.ts ./
COPY postcss.config.js tailwind.config.ts biome.json ./
COPY index.html ./
COPY public ./public
COPY src ./src
# Build and clean artifacts
ENV NODE_ENV=production
RUN pnpm build && \
find dist -type f \( -name "*.map" -o -name ".*" \) -delete && \
rm -f dist/stats.html
BAD: Tag-based (mutable)
FROM node:20-alpine
GOOD: Digest-pinned (immutable)
FROM node:20-alpine@sha256:2d5e8a8a...
Giải thích:
20-alpine) có thể thay đổi → supply chain attacksha256:...) immutable → reproducible builds"Always use digest-pinned images in production to prevent tag mutation attacks"
- OWASP Docker Security Cheat Sheet
Cách thông thường:
RUN npm install -g pnpm@9.12.2
Cách tối ưu:
RUN corepack enable && \
corepack prepare pnpm@9.12.2 \
--activate
Lợi ích:
COPY package.json pnpm-lock.yaml .npmrc ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
pnpm install --frozen-lockfile --prefer-offline
Giải thích từng phần:
--mount=type=cache: BuildKit cache mountid=pnpm: Cache ID (unique per project)target=/pnpm/store: Mount vào pnpm store location--frozen-lockfile: Không update lockfile (reproducible)--prefer-offline: Ưu tiên cache localBuild lần 1 (cold cache):
├─ Load base images: 3s
├─ Download packages: 15s
├─ Install deps: 15s
├─ Build app: 34s
└─ Total: ~64s
Build lần 2 (warm cache):
├─ All stages cached
└─ Total: 0.2s (99.7% faster)
Cache mount không persist trong image (không tăng size)
GOOD: Separate dependencies from source
COPY package.json pnpm-lock.yaml ./
RUN pnpm install
COPY src ./src
RUN pnpm build
BAD: Copy everything, breaks cache
COPY . ./
RUN pnpm install && pnpm build
Giải thích Docker Layer Caching:
Kết quả: 90% builds chỉ rebuild source, không reinstall deps
ENV NODE_ENV=production
RUN pnpm build && \
find dist -type f \
\( -name "*.map" -o -name ".*" \) -delete && \
rm -f dist/stats.html
Artifacts được xóa:
*.map: Source maps (12-15 MB).DS_Store, .gitkeep: Hidden filesstats.html: Webpack bundle analyzerLý do:
Mục đích: Compile nginx thành static binary với minimal modules
FROM alpine:${ALPINE_VERSION}@sha256:... AS nginx-builder
ARG NGINX_VERSION
ARG TARGETPLATFORM
ARG BUILDPLATFORM
ENV NGINX_SHA256=...
# Log build platform info for multi-arch
RUN echo "Building on $BUILDPLATFORM for $TARGETPLATFORM"
# Install build dependencies
RUN apk add --no-cache \
gcc g++ musl-dev make linux-headers curl \
pcre-dev pcre2-dev zlib-dev zlib-static \
openssl-dev openssl-libs-static upx
# Download and verify nginx
WORKDIR /tmp
RUN curl -fSL "https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz" -o nginx.tar.gz && \
echo "${NGINX_SHA256} nginx.tar.gz" | sha256sum -c -
# Build fully static nginx with minimal modules
RUN tar -xzf nginx.tar.gz && \
cd "nginx-${NGINX_VERSION}" && \
./configure \
--prefix=/usr/local/nginx \
--sbin-path=/usr/local/nginx/sbin/nginx \
--conf-path=/etc/nginx/nginx.conf \
--pid-path=/run/nginx.pid \
--lock-path=/run/nginx.lock \
--error-log-path=/dev/stderr \
--http-log-path=/dev/stdout \
--user=nobody \
--group=nobody \
# Performance features
--with-threads \
--with-file-aio \
--with-http_ssl_module \
--with-http_v2_module \
--with-http_gzip_static_module \
--with-http_stub_status_module \
--with-pcre \
--with-pcre-jit \
# Static linking and optimization
--with-cc-opt='-static -Os -ffunction-sections -fdata-sections' \
--with-ld-opt='-static -Wl,--gc-sections' \
# Disable unnecessary modules
--without-http_charset_module \
--without-http_ssi_module \
--without-http_userid_module \
--without-http_auth_basic_module \
--without-http_mirror_module \
--without-http_autoindex_module \
--without-http_geo_module \
--without-http_map_module \
--without-http_split_clients_module \
--without-http_referer_module \
--without-http_rewrite_module \
--without-http_proxy_module \
--without-http_fastcgi_module \
--without-http_uwsgi_module \
--without-http_scgi_module \
--without-http_grpc_module \
--without-http_memcached_module \
--without-http_limit_conn_module \
--without-http_limit_req_module \
--without-http_empty_gif_module \
--without-http_browser_module \
--without-http_upstream_hash_module \
--without-http_upstream_ip_hash_module \
--without-http_upstream_least_conn_module \
--without-http_upstream_random_module \
--without-http_upstream_keepalive_module \
--without-http_upstream_zone_module \
--without-mail_pop3_module \
--without-mail_imap_module \
--without-mail_smtp_module \
--without-stream_limit_conn_module \
--without-stream_access_module \
--without-stream_geo_module \
--without-stream_map_module \
--without-stream_split_clients_module \
--without-stream_return_module \
--without-stream_set_module \
--without-stream_upstream_hash_module \
--without-stream_upstream_least_conn_module \
--without-stream_upstream_random_module \
--without-stream_upstream_zone_module && \
make -j"$(nproc)" && \
make install
# Optimize binary: strip symbols + UPX compression
RUN strip --strip-all /usr/local/nginx/sbin/nginx && \
upx --best --lzma /usr/local/nginx/sbin/nginx && \
/usr/local/nginx/sbin/nginx -V
ENV NGINX_SHA256=627fe086209bba80a2853a0add9d958d7ebbdffa1a8467a5784c9a6b4f03d738
RUN curl -fSL \
"https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz" \
-o nginx.tar.gz && \
echo "${NGINX_SHA256} nginx.tar.gz" | sha256sum -c -
Giải thích:
sha256sum -c: Verify checksumBảo vệ khỏi:
./configure \
...
# Performance features
--with-threads \
--with-file-aio \
--with-http_ssl_module \
--with-http_v2_module \
--with-http_gzip_static_module \
--with-http_stub_status_module \
--with-pcre \
--with-pcre-jit \
# Disable unnecessary modules
--without-http_charset_module \
--without-http_ssi_module \
--without-http_userid_module \
--without-http_auth_basic_module \
--without-http_mirror_module \
--without-http_autoindex_module \
--without-http_geo_module \
--without-http_map_module \
--without-http_split_clients_module \
--without-http_referer_module \
--without-http_rewrite_module \
--without-http_proxy_module \
--without-http_fastcgi_module \
--without-http_uwsgi_module \
--without-http_scgi_module \
--without-http_grpc_module \
--without-http_memcached_module \
--without-http_limit_conn_module \
--without-http_limit_req_module \
--without-http_empty_gif_module \
--without-http_browser_module \
--without-http_upstream_hash_module \
--without-http_upstream_ip_hash_module \
--without-http_upstream_least_conn_module \
--without-http_upstream_random_module \
--without-http_upstream_keepalive_module \
--without-http_upstream_zone_module && \
make -j"$(nproc)" && \
make install
Proxy/Backend modules (không cần cho static site):
http_proxy_module: Reverse proxyhttp_fastcgi_module: FastCGIhttp_uwsgi_module: uWSGIhttp_grpc_module: gRPCAuthentication modules (không cần):
http_auth_basic_module: Basic authhttp_auth_request_module: Auth subrequestAdvanced routing (không cần):
http_rewrite_module: URL rewritinghttp_geo_module: Geo-based routingKết quả: nginx binary từ 22 MB → 5.2 MB (76% smaller)
./configure \
--with-cc-opt='-static -Os -ffunction-sections -fdata-sections' \
--with-ld-opt='-static -Wl,--gc-sections'
Giải thích chi tiết:
Compiler Flags (--with-cc-opt):
-static: Link static libraries-Os: Optimize for Size-ffunction-sections: Each function in separate section-fdata-sections: Each data in separate sectionLinker Flags (--with-ld-opt):
-static: Create static binary-Wl,--gc-sections: Remove unused sectionsDYNAMIC LINKING:
nginx binary: 1.2 MB
+ libc.so: 2.1 MB
+ libssl.so: 3.5 MB
+ libpcre.so: 0.8 MB
+ libz.so: 0.3 MB
─────────────────────────────
Total: 7.9 MB
Dependencies: 5 files
STATIC LINKING:
nginx binary: 5.2 MB
─────────────────────────────
Total: 5.2 MB
Dependencies: 0 files ✓
Trade-off:
# Strip symbols
RUN strip --strip-all /usr/local/nginx/sbin/nginx
# UPX compression
RUN upx --best --lzma /usr/local/nginx/sbin/nginx
# Verify still works
RUN /usr/local/nginx/sbin/nginx -V
Chi tiết từng bước:
| Advantages | Disadvantages |
|---|---|
| 56% size reduction | +50ms startup |
| Faster download | +2MB RAM usage |
| Less disk usage | Slower first run |
| Faster deployment |
Mục đích: Pre-compress static assets (gzip + brotli) để nginx serve trực tiếp
FROM alpine:${ALPINE_VERSION}@sha256:... AS compressor
RUN apk add --no-cache brotli gzip findutils
WORKDIR /app
COPY --from=builder /app/dist ./dist
# Parallel compression: gzip + brotli for all text-based assets
RUN find dist -type f \
\( -name "*.html" -o -name "*.css" -o -name "*.js" -o \
-name "*.json" -o -name "*.svg" -o -name "*.xml" \) \
-print0 | xargs -0 -P"$(nproc)" -I {} sh -c 'gzip -9 -k -f "{}" && brotli -q 11 -f "{}"'
Runtime compression (traditional):
# Nginx compress on-the-fly
gzip on;
gzip_types text/css application/javascript;
Nhược điểm:
Pre-compression (our approach):
# Compress during build
gzip -9 index.html → index.html.gz
brotli -q 11 index.html → index.html.br
# Nginx serve pre-compressed
gzip_static on;
Ưu điểm:
File: index.html
├─ Original: 14,523 bytes
├─ Gzip -9: 3,891 bytes (73% smaller)
└─ Brotli-11: 3,247 bytes (78% smaller)
File: main.js (React + ReactDOM)
├─ Original: 245,832 bytes
├─ Gzip -9: 68,234 bytes (72% smaller)
└─ Brotli-11: 61,445 bytes (75% smaller)
File: styles.css (Tailwind CSS)
├─ Original: 42,156 bytes
├─ Gzip -9: 8,923 bytes (79% smaller)
└─ Brotli-11: 7,834 bytes (81% smaller)
Trung bình: Brotli tốt hơn Gzip 5-10%, cả hai đều giảm ~75% network transfer
find dist -type f \( -name "*.js" ... \) -print0 | \
xargs -0 -P"$(nproc)" -I {} sh -c \
'gzip -9 -k "{}" && brotli -q 11 "{}"'
Giải thích từng phần:
find dist -type f: Tìm tất cả files-name "*.js" -o -name "*.css": Filter theo extension-print0: Null-terminated (handle spaces)xargs -0: Read null-terminated-P"$(nproc)": Parallel = số CPU cores-I {}: Replace stringsh -c '...': Execute shell commandSequential compression (1 core):
├─ 150 files × 0.5s = 75 seconds
Parallel compression (8 cores):
├─ 150 files / 8 cores = 19 files/core
├─ 19 files × 0.5s = 9.5 seconds
└─ Speedup: 7.9x
Actual time:
├─ Sequential: 78s
└─ Parallel: 11s (7.1x faster)
Mục đích: Chuẩn bị filesystem tối thiểu cho FROM scratch
FROM alpine:${ALPINE_VERSION}@sha256:... AS rootfs
# Create directory structure
RUN mkdir -p \
/rootfs/etc/nginx/conf.d \
/rootfs/usr/share/nginx/html \
/rootfs/var/log/nginx \
/rootfs/var/cache/nginx \
/rootfs/usr/local/nginx/{client_body,proxy,fastcgi,uwsgi,scgi}_temp \
/rootfs/tmp \
/rootfs/run && \
chmod 1777 /rootfs/tmp
# Create minimal user database (nobody user)
RUN echo "nobody:x:65534:65534:nobody:/:/sbin/nologin" > /rootfs/etc/passwd && \
echo "nobody:x:65534:" > /rootfs/etc/group
# Copy nginx configuration
COPY nginx.conf /rootfs/etc/nginx/conf.d/default.conf
COPY --from=nginx-builder /etc/nginx/mime.types /rootfs/etc/nginx/mime.types
COPY --from=compressor /app/dist /rootfs/usr/share/nginx/html
# Create main nginx.conf
RUN cat > /rootfs/etc/nginx/nginx.conf <<'EOF'
worker_processes auto;
error_log stderr warn;
pid /run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /dev/stdout;
# Performance optimizations
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
server_tokens off;
# Compression
gzip on;
gzip_static on;
gzip_vary on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
include /etc/nginx/conf.d/*.conf;
}
EOF
# Set proper ownership
RUN chown -R 65534:65534 \
/rootfs/usr/share/nginx/html \
/rootfs/var/log/nginx \
/rootfs/var/cache/nginx \
/rootfs/usr/local/nginx \
/rootfs/tmp \
/rootfs/run
Vấn đề: FROM scratch không có filesystem
Giải pháp: Chuẩn bị rootfs trong Alpine với đầy đủ cấu trúc thư mục
# Build rootfs in Alpine với tất cả directories nginx cần
RUN mkdir -p \
/rootfs/etc/nginx/conf.d \
/rootfs/usr/share/nginx/html \
/rootfs/var/log/nginx \
/rootfs/var/cache/nginx \
/rootfs/usr/local/nginx/{client_body,proxy,fastcgi,uwsgi,scgi}_temp \
/rootfs/tmp \
/rootfs/run
# Copy to final scratch image
FROM scratch
COPY --from=rootfs /rootfs /
Giải thích:
/rootfs trong Alpine là "staging area"# Create nobody user (UID 65534)
RUN echo "nobody:x:65534:65534:nobody:/:/sbin/nologin" \
> /rootfs/etc/passwd && \
echo "nobody:x:65534:" > /rootfs/etc/group
Giải thích /etc/passwd format:
username:password:UID:GID:comment:home:shell
nobody:x:65534:65534:nobody:/:/sbin/nologin
• x → password in /etc/shadow (not used)
• 65534 → UID (standard nobody UID)
• 65534 → GID
• /sbin/nologin → no shell (security)
RUN chown -R 65534:65534 \
/rootfs/usr/share/nginx/html \
/rootfs/var/log/nginx \
/rootfs/var/cache/nginx \
/rootfs/usr/local/nginx \
/rootfs/tmp \
/rootfs/run
Tại sao cần chown?
Final image run as USER 65534:
FROM scratch
COPY --from=rootfs /rootfs /
USER 65534:65534
Nginx (UID 65534) cần write permission:
/var/log/nginx: Access logs/run/nginx.pid: PID file/tmp: Temp files| Directory | Owner | Mode | Purpose |
|---|---|---|---|
| /etc/nginx | root | 0755 | Read-only config |
| /usr/share/nginx/html | 65534 | 0755 | Static files |
| /var/log/nginx | 65534 | 0755 | Logs (stdout/err) |
| /run | 65534 | 0755 | PID file |
| /tmp | 65534 | 1777 | Temp + sticky bit |
| /usr/local/nginx/*_temp | 65534 | 0755 | Nginx temp dirs |
Sticky bit (1777) on /tmp: Chỉ owner có thể delete files
Mục đích: Tạo minimal production image với zero attack surface
FROM scratch
# Re-declare build args for metadata
ARG BUILD_DATE
ARG GIT_COMMIT=unknown
# OCI metadata labels
LABEL org.opencontainers.image.title="Vite React - Distroless" \
org.opencontainers.image.description="Distroless minimal image (<6MB) - UPX compressed" \
org.opencontainers.image.version="2.2.0-distroless-upx" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.revision="${GIT_COMMIT}" \
org.opencontainers.image.base.name="scratch" \
org.opencontainers.image.source="https://github.com/riipandi/vite-react-template" \
org.opencontainers.image.licenses="MIT OR Apache-2.0" \
maintainer="contest-2025-optimized"
# Copy static nginx binary and minimal filesystem
COPY --from=nginx-builder /usr/local/nginx/sbin/nginx /usr/sbin/nginx
COPY --from=rootfs /rootfs /
# Run as non-root user (nobody = UID 65534)
USER 65534:65534
EXPOSE 3000
# Lightweight healthcheck using nginx config test
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["/usr/sbin/nginx", "-t", "-q"]
STOPSIGNAL SIGTERM
ENTRYPOINT ["/usr/sbin/nginx"]
CMD ["-g", "daemon off;"]
Giải thích FROM scratch:
FROM alpine → 7.5 MB base + 100+ packages
FROM debian → 124 MB base + 400+ packages
FROM scratch → 0 bytes, không có OS
FROM scratch chỉ chứa:
Our case:
| Feature | Alpine | Scratch |
|---|---|---|
| Shell | /bin/sh | None |
| Utils | ~50 bins | None |
| Package | apk | None |
| Libraries | ~100 | None |
| CVEs | 18-57 | 0 |
"The best defense is to have nothing to attack"
- Defense in Depth principle
LABEL org.opencontainers.image.title="Vite React" \
org.opencontainers.image.version="2.2.0" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.revision="${GIT_COMMIT}" \
org.opencontainers.image.base.name="scratch" \
org.opencontainers.image.licenses="MIT"
OCI Image Spec: Standard metadata format
Lợi ích của labels:
# Inspect image metadata
docker inspect app:contest | jq '.[0].Config.Labels'
# Filter images by label
docker images --filter "label=org.opencontainers.image.title=Vite React"
# Used by registries
# Docker Hub, Harbor, Quay.io display labels
Best practice: Include version, created date, commit hash
USER 65534:65534
Security principle: Least privilege
65534 = nobody:
Running as root
USER root # or không có USER directive
→ UID 0, có thể làm mọi thứ trong container
Running as nobody
USER 65534:65534
→ UID 65534, chỉ write vào owned dirs
Kịch bản khai thác:
1. Kẻ tấn công tìm thấy lỗ hổng RCE (Remote Code Execution) trên nginx
2. Thực thi lệnh shell với quyền USER
Root: → Có thể ghi đè /usr/sbin/nginx, leo thang đặc quyền
Nobody: → Không thể ghi vào /usr/sbin, thiệt hại bị giới hạn
HEALTHCHECK --interval=30s \
--timeout=3s \
--start-period=5s \
--retries=3 \
CMD ["/usr/sbin/nginx", "-t", "-q"]
Giải thích từng parameter:
--interval=30s: Check mỗi 30 giây--timeout=3s: Command timeout--start-period=5s: Grace period sau start--retries=3: Fail sau 3 lần retryHTTP healthcheck (common)
HEALTHCHECK CMD curl -f http://localhost:3000/ || exit 1
Vấn đề:
Config test (lightweight)
CMD ["/usr/sbin/nginx", "-t", "-q"]
Ưu điểm:
Real build output - Cold build (64s) vs Warm build (0.2s)