To optimize Docker images, you can do both: structure and content. Here are some approaches:
Minimal Base Image Use a small base image, such as alpine, which has an order of magnitude smaller footprint than ubuntu or debian. For example:

Multi-stage builds: build your application in stages, keeping the essence of your application in the final image. This is particularly useful for when your application is written in a language that is either Java, Go, or Node.js, as these are languages where you generally have build dependencies that you will only need temporarily:

Cleanup Unused Files: Delete cache, build artifacts, and other unused files after installing. For example, when running apt or apk, use --no-cache to avoid creating gigantic images.
Reduce Layers by Combining Commands Multiple RUN commands can build an image, reducing the number of layers by combining them. For example:

Use .dockerignore: Exclude from the build context files that do not need to exist to make the image by using a file called .dockerignore, just like you have a .gitignore. By these actions you could considerably decrease your image size and get it to run so much better in production.