Skip to content

system - optimize docker image size#174

Merged
mitch10593 merged 2 commits intomainfrom
feature/140-optimize-docker-images-size
Mar 12, 2026
Merged

system - optimize docker image size#174
mitch10593 merged 2 commits intomainfrom
feature/140-optimize-docker-images-size

Conversation

@mitch10593
Copy link
Copy Markdown
Contributor

@mitch10593 mitch10593 commented Mar 12, 2026

Summary by Sourcery

Optimize backend Docker image by introducing a multi-stage build with a slim runtime image and updating metadata.

Enhancements:

  • Refine backend Dockerfile to use a dedicated build stage and a leaner production runtime image without build tooling.
  • Ensure production image runs under a non-root worker user with appropriate working directory and environment configuration.
  • Update changelog to record the Docker image optimization change.

Build:

  • Adjust backend Docker build setup and .dockerignore files to reduce image size and build context.

@mitch10593 mitch10593 added this to the 2.1 milestone Mar 12, 2026
@mitch10593 mitch10593 self-assigned this Mar 12, 2026
@mitch10593 mitch10593 linked an issue Mar 12, 2026 that may be closed by this pull request
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai bot commented Mar 12, 2026

Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Refactors the backend Docker build into a multi-stage process that installs dependencies in an intermediate image and copies only the runtime artifacts into a slimmer production image, along with updating metadata and changelog, and touching Docker ignore files.

Flow diagram for backend Docker image build and runtime separation

flowchart TD
    A["Start docker build for backend"] --> B["Build base stage\nFROM python:3.12-slim\nInstall uv\nSet WORKDIR /app\nConfigure dev entrypoint/cmd"]
    B --> C["Build build stage\nFROM base"]
    C --> D["Copy pyproject.toml and uv.lock* into image"]
    D --> E["Run uv sync --frozen --no-dev --no-install-project\nInstall dependencies into virtualenv"]
    E --> F["Copy full backend source tree into image"]
    F --> G["Write APP_VERSION into VERSION file"]
    G --> H["Run uv sync --frozen --no-dev\nInstall project into virtualenv"]

    H --> I["Build prod stage\nFROM python:3.12-slim"]
    I --> J["Create worker group (gid 1000) and user (uid 1000)"]
    J --> K["Set WORKDIR /app and ENV PATH=/app/.venv/bin:$PATH"]
    K --> L["Copy /app from build stage\nSet ownership to worker:worker"]
    L --> M["Set USER worker and EXPOSE 8000"]

    M --> N["Final prod image created\nSlim runtime without uv\nReady for deployment"]
Loading

File-Level Changes

Change Details Files
Refactor backend Dockerfile into a multi-stage build to reduce final image size and remove build-only tooling from the production image.
  • Rename the previous prod stage to an intermediate build stage that installs dependencies and project sources using uv.
  • Introduce a new final prod stage based on python:3.12-slim without uv, copying only the prepared /app directory from the build stage.
  • Set APP_VERSION during the build stage and perform a second frozen, no-dev dependency sync after writing the version file.
  • Switch from root to a dedicated non-root worker user and group with an application home directory.
  • Configure WORKDIR and PATH so the application runs from the virtualenv in /app/.venv and uses the copied application files.
backend/Dockerfile
Document the Docker image size optimization in the changelog.
  • Append a changelog entry describing the Docker image size optimization under the unreleased section.
CHANGELOG.md
Adjust backend and frontend Docker ignore behavior to better support leaner images (specific patterns not shown in diff).
  • Update or add backend .dockerignore to exclude unnecessary files from the backend image build context.
  • Update or add frontend .dockerignore to exclude unnecessary files from the frontend image build context.
backend/.dockerignore
frontend/.dockerignore

Assessment against linked issues

Issue Objective Addressed Explanation
#140 Optimize backend Docker image size by restructuring the Dockerfile (e.g., separate dependency installation from application source, and/or use multi-stage builds).
#140 Reduce Docker build context size for backend and frontend images by refining .dockerignore files, indirectly contributing to smaller and more efficient Docker images.

Possibly linked issues

  • #optimize docker images size: PR restructures backend Dockerfile into build and prod stages, installing dependencies before source to shrink images.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • The final prod stage no longer defines an ENTRYPOINT or CMD, so the runtime container will not start the API as before; consider copying the relevant declarations from the base stage or explicitly defining them again.
  • You’re re-running uv sync in both the build stage (with --no-install-project) and again after writing VERSION; if possible, consolidate this into a single uv sync invocation to avoid redundant work in the image build.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The final `prod` stage no longer defines an `ENTRYPOINT` or `CMD`, so the runtime container will not start the API as before; consider copying the relevant declarations from the `base` stage or explicitly defining them again.
- You’re re-running `uv sync` in both the `build` stage (with `--no-install-project`) and again after writing `VERSION`; if possible, consolidate this into a single `uv sync` invocation to avoid redundant work in the image build.

## Individual Comments

### Comment 1
<location path="backend/Dockerfile" line_range="34-43" />
<code_context>
+RUN echo "$APP_VERSION" > VERSION && uv sync --frozen --no-dev
+
+# ---- Prod: clean runtime image without uv ----
+FROM python:3.12-slim AS prod

 LABEL org.opencontainers.image.source="https://github.com/VEAF/website-2026"
 LABEL org.opencontainers.image.description="VEAF Website 2026 - Backend API"

-COPY pyproject.toml uv.lock* ./
-RUN uv sync --frozen --no-dev --no-install-project
+RUN groupadd --gid 1000 worker && useradd --uid 1000 --gid worker --create-home worker

-COPY . .
-RUN echo "$APP_VERSION" > VERSION
-RUN uv sync --frozen --no-dev
+WORKDIR /app
+ENV PATH="/app/.venv/bin:$PATH"
+
+COPY --from=build --chown=worker:worker /app /app

-RUN chown -R worker:worker /app
 USER worker
 EXPOSE 8000

</code_context>
<issue_to_address>
**issue (bug_risk):** The prod stage no longer inherits ENTRYPOINT/CMD from `base`, so the final image lacks a startup command.

Since `prod` now uses `FROM python:3.12-slim` instead of `FROM base`, it no longer gets the `ENTRYPOINT ["/app/entrypoint.sh"]` and uvicorn `CMD`. The image will build but run without a default startup command, changing behavior from the previous setup.

To preserve the prior behavior, either:
- Define the same `ENTRYPOINT`/`CMD` in the `prod` stage, or
- Introduce a minimal base stage that sets the entrypoint/command and have `prod` inherit from it, keeping `uv` only in the build stage.
</issue_to_address>

### Comment 2
<location path="backend/Dockerfile" line_range="44" />
<code_context>
+WORKDIR /app
+ENV PATH="/app/.venv/bin:$PATH"
+
+COPY --from=build --chown=worker:worker /app /app

-RUN chown -R worker:worker /app
</code_context>
<issue_to_address>
**suggestion:** Copying the entire `/app` tree into prod may undermine the goal of a minimal runtime image.

This `COPY` pulls the entire build context into the runtime image, including build-only artifacts (`uv.lock*`, caches, tests, dev-only files, possibly `.git`, etc., depending on `.dockerignore`). Consider copying only the virtualenv, app code, `entrypoint.sh`, `VERSION`, and other explicit runtime assets to keep the image small, reduce surface area, and make runtime dependencies clearer.

Suggested implementation:

```
# Copy only runtime dependencies from the build stage to keep the image minimal.
# Adjust the paths below to match the layout produced in the build stage.
/* Python virtual environment */
COPY --from=build --chown=worker:worker /app/.venv /app/.venv

/* Application code */
COPY --from=build --chown=worker:worker /app/app /app/app

/* Entrypoint / startup scripts */
COPY --from=build --chown=worker:worker /app/entrypoint.sh /app/entrypoint.sh

/* Version / metadata files */
COPY --from=build --chown=worker:worker /app/VERSION /app/VERSION

```

1. Make sure the paths (`/app/app`, `/app/entrypoint.sh`, `/app/VERSION`) match the actual structure produced by the `build` stage (e.g., if your module is `backend`, you might need `/app/backend` instead of `/app/app`).
2. If there are additional runtime assets (config files, static files, migrations, etc.), add explicit `COPY --from=build` lines for those as well.
3. Ensure `entrypoint.sh` is marked executable in the build or runtime stage (e.g., `RUN chmod +x /app/entrypoint.sh` if not already handled).
4. Confirm `.dockerignore` excludes dev-only and build-only artifacts from the build context so they can’t accidentally leak into other layers.
</issue_to_address>

### Comment 3
<location path="CHANGELOG.md" line_range="12" />
<code_context>
 - ADDED login/logout notifications
 - FIXED system - customer deploy stack name
 - ADDED calandar - planning view (default for mobile)
+- CHANGED system - optimize docker image size
</code_context>
<issue_to_address>
**issue (typo):** Typo: "calandar" should be "calendar".

Please correct the spelling to "calendar" in this changelog entry.

```suggestion
- ADDED calendar - planning view (default for mobile)
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +34 to +43
FROM python:3.12-slim AS prod

LABEL org.opencontainers.image.source="https://github.com/VEAF/website-2026"
LABEL org.opencontainers.image.description="VEAF Website 2026 - Backend API"

COPY pyproject.toml uv.lock* ./
RUN uv sync --frozen --no-dev --no-install-project
RUN groupadd --gid 1000 worker && useradd --uid 1000 --gid worker --create-home worker

COPY . .
RUN echo "$APP_VERSION" > VERSION
RUN uv sync --frozen --no-dev
WORKDIR /app
ENV PATH="/app/.venv/bin:$PATH"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The prod stage no longer inherits ENTRYPOINT/CMD from base, so the final image lacks a startup command.

Since prod now uses FROM python:3.12-slim instead of FROM base, it no longer gets the ENTRYPOINT ["/app/entrypoint.sh"] and uvicorn CMD. The image will build but run without a default startup command, changing behavior from the previous setup.

To preserve the prior behavior, either:

  • Define the same ENTRYPOINT/CMD in the prod stage, or
  • Introduce a minimal base stage that sets the entrypoint/command and have prod inherit from it, keeping uv only in the build stage.

WORKDIR /app
ENV PATH="/app/.venv/bin:$PATH"

COPY --from=build --chown=worker:worker /app /app
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Copying the entire /app tree into prod may undermine the goal of a minimal runtime image.

This COPY pulls the entire build context into the runtime image, including build-only artifacts (uv.lock*, caches, tests, dev-only files, possibly .git, etc., depending on .dockerignore). Consider copying only the virtualenv, app code, entrypoint.sh, VERSION, and other explicit runtime assets to keep the image small, reduce surface area, and make runtime dependencies clearer.

Suggested implementation:

# Copy only runtime dependencies from the build stage to keep the image minimal.
# Adjust the paths below to match the layout produced in the build stage.
/* Python virtual environment */
COPY --from=build --chown=worker:worker /app/.venv /app/.venv

/* Application code */
COPY --from=build --chown=worker:worker /app/app /app/app

/* Entrypoint / startup scripts */
COPY --from=build --chown=worker:worker /app/entrypoint.sh /app/entrypoint.sh

/* Version / metadata files */
COPY --from=build --chown=worker:worker /app/VERSION /app/VERSION

  1. Make sure the paths (/app/app, /app/entrypoint.sh, /app/VERSION) match the actual structure produced by the build stage (e.g., if your module is backend, you might need /app/backend instead of /app/app).
  2. If there are additional runtime assets (config files, static files, migrations, etc.), add explicit COPY --from=build lines for those as well.
  3. Ensure entrypoint.sh is marked executable in the build or runtime stage (e.g., RUN chmod +x /app/entrypoint.sh if not already handled).
  4. Confirm .dockerignore excludes dev-only and build-only artifacts from the build context so they can’t accidentally leak into other layers.

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
@mitch10593 mitch10593 merged commit 5fb86fc into main Mar 12, 2026
3 checks passed
@mitch10593 mitch10593 deleted the feature/140-optimize-docker-images-size branch March 12, 2026 19:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

optimize docker images size

1 participant