Dockerfile.full
Rama main · réplica de lectura del repositorio original.
FROM python:3.13-slim AS builder
# Full variant — Debian/glibc base so DuckDB and psycopg2 install from
# published manylinux wheels instead of building from source against musl.
# See issue #411: DuckDB has no musllinux wheels and runs ~30% slower
# on musl per upstream docs; psycopg2-binary likewise has no musl wheels.
# awscli 1.45.x pins botocore exactly and 1.46 bundles its own copy (~120 MB),
# so the CLI is installed without deps and runs on the botocore pinned here.
RUN pip install --no-cache-dir --no-compile \
hypercorn==0.18.0 \
"cbor2>=5.4.0" \
"defusedxml>=0.7" \
"docker>=7.0.0" \
"pyyaml>=6.0" \
"cryptography>=41.0" \
"pymysql>=1.1" \
"psycopg2-binary>=2.9" \
"duckdb>=0.10.0" \
"asyncssh>=2.14" \
"aws-sam-translator>=1.90" \
"boto3>=1.34" \
"jsonata-python>=0.7.0" \
"graphql-core==3.2.12" \
"botocore==1.43.124" \
"docutils>=0.18.1,<=0.19" \
"colorama>=0.2.5,<0.4.7" \
"rsa>=3.1.2,<4.8" \
&& pip install --no-cache-dir --no-compile --no-deps awscli==1.45.63
# Strip awscli help examples (~25 MB), Python cache files (~15 MB),
# pip metadata, and DuckDB stub / ADBC artefacts that aren't used at runtime.
RUN rm -rf /usr/local/lib/python3.13/site-packages/awscli/examples \
&& find /usr/local/lib/python3.13/site-packages -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null \
&& rm -rf /usr/local/lib/python3.13/site-packages/pip*.dist-info \
&& rm -rf /usr/local/lib/python3.13/site-packages/pip* \
&& rm -rf /usr/local/lib/python3.13/site-packages/_duckdb-stubs \
&& rm -rf /usr/local/lib/python3.13/site-packages/adbc_driver_duckdb
# Compile the IAM-auth compatibility plugin against each supported MySQL
# series. The compiler and headers remain isolated in these build stages.
FROM mysql:8.0 AS plugin-build-80
USER root
RUN mysql_version="$(rpm -q --qf '%{VERSION}-%{RELEASE}' mysql-community-server-minimal)" \
&& sed -i 's#/docker/#/#' /etc/yum.repos.d/mysql-community-minimal.repo \
&& microdnf install -y gcc-c++ "mysql-community-debugsource-${mysql_version}" \
&& microdnf clean all
ARG TARGETARCH
COPY contrib/mysql-iam-plugin/aws_auth_plugin.cc /tmp/aws_auth_plugin.cc
RUN arch="${TARGETARCH}"; \
if [ -z "${arch}" ]; then \
case "$(uname -m)" in \
x86_64) arch=amd64 ;; \
aarch64) arch=arm64 ;; \
*) echo "unsupported build architecture: $(uname -m)" >&2; exit 1 ;; \
esac; \
fi; \
header="$(rpm -ql mysql-community-debugsource | grep '/include/mysql/plugin_auth.h$' | head -1)"; \
include_dir="$(dirname "$(dirname "${header}")")"; \
mkdir -p "/opt/ministack/mysql-plugins/8.0/${arch}"; \
g++ -O2 -Wall -Wextra -Werror -fPIC -shared \
-DMYSQL_ABI_CHECK -DMYSQL_DYNAMIC_PLUGIN \
-I"${include_dir}" \
-o "/opt/ministack/mysql-plugins/8.0/${arch}/aws_auth_plugin.so" \
/tmp/aws_auth_plugin.cc; \
nm -D "/opt/ministack/mysql-plugins/8.0/${arch}/aws_auth_plugin.so" \
| grep -q ' _mysql_plugin_interface_version_$'
FROM mysql:8.4 AS plugin-build
USER root
RUN mysql_version="$(rpm -q --qf '%{VERSION}-%{RELEASE}' mysql-community-server-minimal)" \
&& sed -i 's#/docker/#/#' /etc/yum.repos.d/mysql-community-minimal.repo \
&& microdnf install -y gcc-c++ "mysql-community-debugsource-${mysql_version}" \
&& microdnf clean all
ARG TARGETARCH
COPY contrib/mysql-iam-plugin/aws_auth_plugin.cc /tmp/aws_auth_plugin.cc
RUN arch="${TARGETARCH}"; \
if [ -z "${arch}" ]; then \
case "$(uname -m)" in \
x86_64) arch=amd64 ;; \
aarch64) arch=arm64 ;; \
*) echo "unsupported build architecture: $(uname -m)" >&2; exit 1 ;; \
esac; \
fi; \
header="$(rpm -ql mysql-community-debugsource | grep '/include/mysql/plugin_auth.h$' | head -1)"; \
include_dir="$(dirname "$(dirname "${header}")")"; \
mkdir -p "/opt/ministack/mysql-plugins/8.4/${arch}"; \
g++ -O2 -Wall -Wextra -Werror -fPIC -shared \
-DMYSQL_ABI_CHECK -DMYSQL_DYNAMIC_PLUGIN \
-I"${include_dir}" \
-o "/opt/ministack/mysql-plugins/8.4/${arch}/aws_auth_plugin.so" \
/tmp/aws_auth_plugin.cc; \
nm -D "/opt/ministack/mysql-plugins/8.4/${arch}/aws_auth_plugin.so" \
| grep -q ' _mysql_plugin_interface_version_$'
COPY --from=plugin-build-80 /opt/ministack/mysql-plugins/8.0 /opt/ministack/mysql-plugins/8.0
# Rebuild ICU's locale data as English-only. Debian's libicudata.so is 31 MB of
# locale tables and nothing else — statically linked, zero exported symbols —
# and Node is the only binary in the image that links ICU. Alpine packages the
# same upstream ICU release as a 3 MB data file, so wrapping that with ICU's own
# genccode produces a drop-in replacement. Non-English `Intl` formatting falls
# back to English, which is what the Alpine image has always done; a derived
# image can restore it with `apt-get install --reinstall libicu76`.
FROM alpine:3.22 AS icu-data
RUN apk add --no-cache icu-data-en
FROM python:3.13-slim AS icu-build
RUN apt-get update \
&& apt-get install -y --no-install-recommends icu-devtools gcc libc6-dev libicu-dev \
&& rm -rf /var/lib/apt/lists/*
COPY --from=icu-data /usr/share/icu/ /tmp/icu/
RUN set -eux; \
dat="$(find /tmp/icu -name 'icudt*.dat' | head -1)"; \
stem="$(basename "${dat}" .dat)"; \
ver="$(basename "$(dirname "${dat}")")"; \
cd /tmp; \
genccode -e "${stem%l}" -d /tmp "${dat}"; \
gcc -shared -fPIC -O2 -o "/tmp/libicudata.so.${ver}" "/tmp/${stem}_dat.c"; \
strip "/tmp/libicudata.so.${ver}"; \
ln -s "libicudata.so.${ver}" /tmp/libicudata.so.slim
FROM python:3.13-slim AS prep
LABEL maintainer="MiniStack" \
description="Local AWS Service Emulator — full variant with DuckDB (Athena), PostgreSQL, MySQL drivers, and SFTP"
# Upgrade base packages, install runtime essentials (nodejs/bash for
# Lambda Node.js + init scripts), then strip pip and locale/doc payload
# we never use at runtime.
RUN apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y --no-install-recommends nodejs bash \
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/* /tmp/* \
&& rm -rf /usr/local/lib/python3.13/site-packages/pip* /usr/local/bin/pip* \
&& rm -rf /usr/share/doc /usr/share/man /usr/share/info /usr/share/locale
WORKDIR /opt/ministack
# Copy cleaned Python packages and CLI entrypoints from builder.
COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages
COPY --from=builder /usr/local/bin/aws /usr/local/bin/aws
COPY --from=builder /usr/local/bin/aws_completer /usr/local/bin/aws_completer
COPY --from=builder /usr/local/bin/hypercorn /usr/local/bin/hypercorn
COPY bin/awslocal /usr/local/bin/awslocal
RUN chmod +x /usr/local/bin/awslocal
COPY ministack/ ministack/
COPY --from=plugin-build /opt/ministack/mysql-plugins /opt/ministack/mysql-plugins
RUN groupadd -r ministack && useradd -r -g ministack ministack
RUN mkdir -p /tmp/ministack-data/s3 && chown -R ministack:ministack /tmp/ministack-data
RUN mkdir -p /docker-entrypoint-initaws.d/ready.d \
/etc/localstack/init/boot.d \
/etc/localstack/init/ready.d && \
chown -R ministack:ministack /docker-entrypoint-initaws.d /etc/localstack
# Swap in the English-only ICU data, then drop payload no runtime path reads:
# the npm helper packages Debian ships beside node (the keep-list is read out of
# libnode itself, because it aborts at startup if an externalized builtin such as
# undici or acorn-walk is missing), perl (nothing here reverse-depends on it, and
# the dpkg entry goes too so a scanner stops reporting its CVEs against files
# that are gone),
# glibc's legacy iconv converters (Python uses its own codecs; the few kept are
# the ones a C library caller could plausibly ask for) and stdlib corners that
# cannot run here now that pip is stripped. The `test -f` fails the build rather
# than shipping a mismatch if the base's ICU major moves ahead of Alpine's, and
# the node check proves Intl still works after the swap.
COPY --from=icu-build /tmp/libicudata.so.* /tmp/icu-slim/
RUN set -eux; \
target="$(find /usr/lib -type f -name 'libicudata.so.*' | head -1)"; \
slim="/tmp/icu-slim/$(basename "${target}")"; \
test -f "${slim}"; \
cp "${slim}" "${target}"; \
rm -rf /tmp/icu-slim; \
node -e "if (new Date(0).toLocaleDateString('en-US',{month:'long'}) !== 'January') process.exit(1)"; \
gconv="$(dirname "$(find /usr/lib -name 'UTF-16.so' -path '*gconv*' | head -1)")"; \
find "${gconv}" -name '*.so' \
! -name 'UTF-16.so' ! -name 'UTF-32.so' ! -name 'UTF-7.so' \
! -name 'UNICODE.so' ! -name 'ISO8859-1.so' ! -name 'CP1252.so' -delete; \
rm -rf /usr/local/lib/python3.13/ensurepip \
/usr/local/lib/python3.13/idlelib \
/usr/local/lib/python3.13/tkinter \
/usr/local/lib/python3.13/pydoc_data \
/usr/local/lib/python3.13/turtledemo \
/usr/local/lib/python3.13/turtle.py; \
python3 -c "import re,glob,os,shutil; \
lib=glob.glob('/usr/lib/*/libnode.so.*')[0]; \
keep={q.split('/')[4] for q in re.findall(r'/usr/share/nodejs/[A-Za-z0-9@._/-]+', open(lib,'rb').read().decode('latin1'))}; \
[shutil.rmtree(os.path.join('/usr/share/nodejs',d), ignore_errors=True) for d in os.listdir('/usr/share/nodejs') if d not in keep]"; \
rm -rf /usr/bin/perl /usr/bin/perl5.40.1 /usr/lib/*/perl-base \
/usr/lib/*/perl5 /usr/share/perl5 /usr/share/perl; \
dpkg --purge --force-remove-essential --force-depends perl-base 2>/dev/null || true; \
node -e "require('http'); if (typeof fetch !== 'function') process.exit(1)"; \
rm -rf /var/cache/* /var/log/* /tmp/*
# The layers above sum to more than the filesystem they produce: `apt-get
# upgrade` rewrites base files into a new layer, and a later `rm -rf` cannot
# shrink the layer beneath it. Flattening reclaims that, at the cost of layer
# sharing with the base image on pull.
FROM scratch
COPY --from=prep / /
WORKDIR /opt/ministack
VOLUME /docker-entrypoint-initaws.d
VOLUME /etc/localstack/init
ARG MINISTACK_VERSION=dev
ENV MINISTACK_VERSION=${MINISTACK_VERSION} \
MINISTACK_EDITION=full \
GATEWAY_PORT=4566 \
LOG_LEVEL=INFO \
S3_PERSIST=0 \
S3_DATA_DIR=/tmp/ministack-data/s3 \
REDIS_HOST=redis \
REDIS_PORT=6379 \
RDS_BASE_PORT=15432 \
RDS_PERSIST=0 \
DSQL_BASE_PORT=25432 \
DSQL_PERSIST=0 \
DSQL_STRICT=0 \
ELASTICACHE_BASE_PORT=16379 \
LAMBDA_EXECUTOR=local \
USE_SSL=0 \
PYTHONUNBUFFERED=1 \
PYTHONOPTIMIZE=2 \
MALLOC_ARENA_MAX=2
EXPOSE 4566 2222
# Pure Python healthcheck — no curl dependency; USE_SSL for HTTP/HTTPS.
HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=3 \
CMD python -c "import os,ssl,urllib.request as r; t=os.environ.get('USE_SSL','').strip().lower() in ('1','true','yes'); p=os.environ.get('GATEWAY_PORT') or os.environ.get('EDGE_PORT') or '4566'; r.urlopen(('https' if t else 'http')+'://localhost:'+p+'/_ministack/health',context=ssl._create_unverified_context() if t else None)" || exit 1
ENTRYPOINT ["python", "-m", "ministack"]