Best Practices for Using apt-get upgrade in Dockerfiles: How to Balance Security and Stability


2 views

When containerizing applications, especially legacy systems with numerous dependencies, developers often face a crucial decision: whether to include package update commands (apt-get update && apt-get upgrade) in their Dockerfiles. While this seems like a good security practice initially, it can introduce several issues that contradict Docker's core principles.

The fundamental issues with putting updates in Dockerfiles include:

  • Breaking reproducibility: Future builds may install different package versions, potentially breaking your application
  • Increased image size: Each update downloads package metadata and new versions
  • Security scanning challenges: Makes it harder to track vulnerabilities in your base image

Instead of generic updates, explicitly specify package versions:

FROM ubuntu:20.04

# Bad practice
# RUN apt-get update && apt-get upgrade -y

# Better practice
RUN apt-get update && \
    apt-get install -y \
    package1=1.2.3 \
    package2=4.5.6-ubuntu1 \
    && rm -rf /var/lib/apt/lists/*

There are limited cases where updates in Dockerfiles make sense:

# Security updates for base image with known vulnerabilities
FROM ubuntu:20.04

# Only update security packages
RUN apt-get update && \
    apt-get upgrade -y --only-upgrade security \
    && rm -rf /var/lib/apt/lists/*

For your "deep freeze" scenario, consider this workflow:

  1. Build and test your container with specific package versions
  2. Tag the image with a version number
  3. Push to a private registry
  4. Set up periodic security scanning on the stored image

Instead of automatic updates in Dockerfiles, implement:

  • Regular rebuilds from pinned versions
  • Automated vulnerability scanning (using tools like Trivy or Clair)
  • CI/CD pipeline that tests new base images before deployment

Here's how a production-grade Dockerfile should handle dependencies:

FROM ubuntu:20.04 AS builder

# Pin all versions explicitly
RUN apt-get update && \
    apt-get install -y \
    python3.8=3.8.10-0ubuntu1~20.04.2 \
    libssl1.1=1.1.1f-1ubuntu2.17 \
    && rm -rf /var/lib/apt/lists/*

# Copy your application files
COPY app /app

# Build your application
RUN make -C /app

# Final stage
FROM ubuntu:20.04

COPY --from=builder /app/bin /usr/local/bin

When containerizing legacy bash applications with numerous dependencies, developers often face the temptation to include package upgrade commands in Dockerfiles. While this might seem like a way to "future-proof" the container, it fundamentally contradicts Docker's immutability principle.

Consider this problematic Dockerfile example:

FROM ubuntu:20.04
RUN apt-get update && apt-get upgrade -y
COPY ./my_legacy_app /app
RUN chmod +x /app/*.sh
CMD ["/app/start.sh"]

The apt-get upgrade command introduces several issues:

  • Creates unpredictable image builds (different package versions each time)
  • Increases image size with unnecessary upgrades
  • Potentially breaks dependency chains your application relies on

Instead of upgrades, explicitly specify package versions:

FROM ubuntu:20.04
RUN apt-get update && \
    apt-get install -y \
    python3.8=3.8.10-0ubuntu1~20.04.2 \
    libcurl4=7.68.0-1ubuntu2.7 \
    && rm -rf /var/lib/apt/lists/*

If security updates are critical, use a multi-stage build to create an update baseline:

FROM ubuntu:20.04 as updater
RUN apt-get update && \
    apt-get upgrade -y --only-upgrade security && \
    apt-mark showmanual > /pkgs.list

FROM ubuntu:20.04
COPY --from=updater /pkgs.list /tmp/
RUN apt-get update && \
    xargs -a /tmp/pkgs.list apt-get install -y --no-install-recommends

For true "deep freezing":

  1. Use specific base image tags (ubuntu:20.04 not ubuntu:latest)
  2. Pin all dependency versions in your Dockerfile
  3. Store known-good images in a private registry
  4. Document the exact OS and package versions in README

Here's an example of a well-structured frozen application Dockerfile:

FROM ubuntu:20.04@sha256:6e5f67d6273e23a0ca9030fc0fb5615fa7a5e824470e2f8698ca2bb3311a8b62

# Pinned runtime dependencies
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    bash=5.0-6ubuntu1.1 \
    coreutils=8.30-3ubuntu2 \ 
    grep=3.4-1 \
    && rm -rf /var/lib/apt/lists/*

# Application files
COPY --chown=root:root ./app /opt/legacy_app

# Fixed entrypoint
ENTRYPOINT ["/opt/legacy_app/start.sh"]