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:
- Build and test your container with specific package versions
- Tag the image with a version number
- Push to a private registry
- 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":
- Use specific base image tags (
ubuntu:20.04
notubuntu:latest
) - Pin all dependency versions in your Dockerfile
- Store known-good images in a private registry
- 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"]