Skip to content
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed
- The Docker image now uses an Alpine base with a multi-stage build, producing a
smaller image (~99 MB). It continues to run as a non-root user.

## [0.3.0] - 2026-06-22

### Added
Expand Down
46 changes: 39 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,58 @@
# always matches the published release of the same version. Pass
# `--build-arg VERSION=X.Y.Z` to pin a specific release; with no VERSION the
# latest release on PyPI is installed.
ARG PYTHON_VERSION=3.10-slim
FROM python:${PYTHON_VERSION}
#
# Notes:
# * Alpine base — small footprint (~99 MB image).
# * Multi-stage build — the compiler toolchain (psycopg2 has no musl wheel and
# is built from source) stays in the builder stage and never reaches the
# runtime image, keeping it minimal.
# * Runs as an unprivileged user.
ARG PYTHON_VERSION=3.13-alpine

# --- Builder: compile psycopg2 and install the package into a venv -----------
FROM python:${PYTHON_VERSION} AS builder

ARG VERSION=

# PostgreSQL + MySQL/MariaDB drivers come from the package extras; SQLite is
# built into Python. `${VERSION:+==${VERSION}}` expands to `==X.Y.Z` only when
# VERSION is set, otherwise installs the latest release.
RUN pip install --no-cache-dir "sql2json[postgres,mysql]${VERSION:+==${VERSION}}"
ENV PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1

# psycopg2-binary ships only glibc (manylinux) wheels, so on Alpine/musl it is
# built from source — that needs a C toolchain and the libpq headers. PyMySQL is
# pure Python and SQLite is built into Python, so no other build deps are needed.
RUN apk add --no-cache build-base postgresql-dev

# `${VERSION:+==${VERSION}}` expands to `==X.Y.Z` only when VERSION is set,
# otherwise installs the latest release.
RUN python -m venv /opt/venv \
&& /opt/venv/bin/pip install --upgrade pip \
&& /opt/venv/bin/pip install "sql2json[postgres,mysql]${VERSION:+==${VERSION}}"

# --- Runtime: minimal Alpine carrying only the venv and libpq ----------------
FROM python:${PYTHON_VERSION} AS runtime

ARG VERSION=

# libpq is the only runtime shared library psycopg2 needs; the build toolchain
# is intentionally left behind in the builder stage.
RUN apk add --no-cache libpq

LABEL org.opencontainers.image.title="sql2json" \
org.opencontainers.image.description="Run SQL queries via SQLAlchemy and output JSON, CSV, or Excel." \
org.opencontainers.image.source="https://github.com/fsistemas/sql2json" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.version="${VERSION}"

# Copy the ready-to-run virtualenv from the builder and put it first on PATH so
# `sql2json` (and `pip`, for the release verify step) resolve from it.
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

# Run as an unprivileged user. The home directory holds the config
# (`/home/app/.sql2json`) and `/workspace` is the writable working dir for
# `--output` files; both are owned by `app`.
RUN useradd --create-home --uid 1000 app \
RUN adduser -D -u 1000 app \
&& mkdir -p /workspace \
&& chown app:app /workspace
USER app
Expand Down
8 changes: 7 additions & 1 deletion RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ requesting one rather than bumping the version yourself.
version has to exist on PyPI first). Both Podman and Docker are shown; either
works.

The image is Alpine-based, and the PostgreSQL driver (`psycopg2`) has no musl
wheel, so the build compiles it from source. This is handled inside the
Dockerfile (the build stage installs the needed toolchain), but it means the
`linux/arm64` leg compiles under QEMU emulation and takes noticeably longer
than a native build — expect a few extra minutes, not a failure.

First, log in to Docker Hub once (use a Docker Hub
[access token](https://hub.docker.com/settings/security) as the password,
not your account password):
Expand Down Expand Up @@ -156,7 +162,7 @@ requesting one rather than bumping the version yourself.

> **Cached-arch base-image gotcha.** After a cross-arch / multi-arch build
> attempt, your local image store can hold a *non-native* base image (e.g. an
> `arm64` `python:3.10-slim` pulled during an `--platform linux/arm64`
> `arm64` `python:3.13-alpine` pulled during an `--platform linux/arm64`
> experiment). Podman will silently reuse that cached base for a subsequent
> plain `podman build`, producing an image that fails to run with
> `exec /bin/sh: Exec format error` and a build-time warning like
Expand Down
Loading