Lesson
Preparing an Image for Production
How to move from a development-friendly Yocto image to a product image that is deliberate, reproducible, and reviewable
A development image is meant to help engineers move quickly.
A production image has a different job. It should boot into the intended product state, expose only the interfaces the product requires, and be reproducible from metadata. That does not mean making the image as small and closed as possible by instinct. It means every user, service, package, login path, and filesystem choice should exist because the product needs it.
A familiar development setting is:
EXTRA_IMAGE_FEATURES += "debug-tweaks"
That kind of setting is useful while bringing a system up. It can make login easier, reduce friction during early testing, and generally stop the machine from fighting you while the real problem is somewhere else. It is also exactly the sort of convenience that should not drift into a product image just because everyone got used to it being there.
Development Convenience Versus Product Policy
The question before release is not “what can we remove?” It is:
What does this product require?
For a bench image, you may want root login, SSH access, a writable root filesystem, debug tools, package management, and permissive login behaviour. For a product image, some of those may still be valid, but they should be conscious choices rather than leftovers.
Review areas such as:
debug-tweaks- empty passwords and root passwords
- root login, especially over SSH
- development SSH keys
- unnecessary users
- unnecessary services
- unnecessary network listeners
- development and debug packages
- compilers, headers, build tools, and interpreters installed only for convenience
- runtime package-management tools where the product does not need them
- test utilities and ptest packages
- debug symbols and
-dbgpackages - writable versus read-only root filesystem assumptions
- logging volume, retention, and destination
- production service enablement
Some products need SSH. Some need a writable area. Some need runtime package management. The point is not to ban those features. The point is to make the reason visible in the project metadata.
Where Production Policy Lives
Production policy should normally be represented in Yocto metadata, not applied manually after the target boots.
That policy may be spread across several mechanisms:
- distro configuration for product-wide policy
DISTRO_FEATURESfor distribution-level capabilitiesIMAGE_FEATURESfor supported image behavioursPACKAGECONFIGfor feature choices inside individual recipes- image contents through
IMAGE_INSTALL - packagegroups for reusable package sets
- service configuration through recipe metadata and init-system classes
This is why the earlier course structure separates BSP, distro, software, and image metadata. A product decision such as “production images do not include debug tools” should not depend on somebody remembering to uninstall packages from a device on a bench.
For systemd services, for example, the product policy should normally come from the recipe and project configuration that install and enable the service. The running target should be the result of the metadata, not a handcrafted exception that only exists on one unit.
Development image versus production image
It is common to keep separate image recipes for the two roles.
| Area | Development image | Production image |
|---|---|---|
| Login | Convenient access, often with temporary credentials | Product-approved authentication only |
| SSH | Usually enabled for engineering access | Enabled only if the product requires it |
| Packages | Debuggers, tracing tools, test utilities, ptests | Packages required for product behaviour |
| Filesystem | Often writable to make investigation easier | Writable only where the product requires it |
| Package management | Useful for installing tools during development | Present only with a supported servicing model |
| Services | May include diagnostic and temporary services | Only intended product services enabled |
| Logging | Verbose, developer-friendly | Sized and retained according to product needs |
I normally prefer this split over trying to make one image behave like two
products through a pile of conditionals. A clear my-product-image and
my-product-dev-image are easier to review, easier for CI to build, and harder
to confuse at release time.
The image lesson introduced this development/production split earlier: Images and Packages.
A Small Configuration Comparison
A development configuration might deliberately include conveniences:
# my-product-dev-image.bb
require my-product-image.bb
EXTRA_IMAGE_FEATURES += "debug-tweaks"
IMAGE_FEATURES += "ssh-server-openssh package-management"
IMAGE_INSTALL:append = " gdbserver strace tcpdump my-product-tests"
This says the development image is based on the production image, but adds access and diagnostic tools. That is a reasonable engineering image. It is not a release candidate.
A production image should express the product policy more directly:
# my-product-image.bb
SUMMARY = "Production image for My Product"
LICENSE = "MIT"
inherit core-image
IMAGE_INSTALL = " \
packagegroup-core-boot \
packagegroup-my-product-runtime \
"
IMAGE_FEATURES += "read-only-rootfs"
IMAGE_FEATURES:remove = "debug-tweaks package-management"
The exact settings are product-specific. A product with a supported field
servicing feed may keep package-management. A device that must store local
state may not use a fully read-only root filesystem. A service appliance may
require SSH, but with controlled keys and login policy.
The important difference is that the production image describes the intended product, while the development image adds engineering support around it.
Controlling Features at the Right Level
Not every decision belongs in the image recipe.
Use distro configuration for policy that applies across the product family. For example, the selected init system, package format, libc choice, and broad distribution features normally belong there rather than being repeated in every image.
Use PACKAGECONFIG when a recipe should build a component with or without a
feature. If a daemon should not include a debug HTTP endpoint, disabling that in
the recipe is better than shipping it and hoping nobody starts it.
Use packagegroups when a set of packages represents a product capability. A
runtime packagegroup such as packagegroup-my-product-runtime is much easier to
review than a long, growing IMAGE_INSTALL list copied between images.
Use service configuration to decide what starts at boot. Installing a package and enabling its service are related, but not identical decisions. A production image should not start diagnostic services merely because they were useful during bring-up.
Writable and Read-Only Filesystems
A read-only root filesystem can make a product easier to reason about because the
base system does not drift during normal operation. It can also expose lazy
assumptions in applications that try to write into /etc, /usr, or other
locations that should be treated as part of the image.
That does not mean every product must have a completely read-only system. Products often need writable data areas for logs, configuration, databases, calibration data, or user content. The engineering question is where those writes belong and how they are managed.
In Yocto terms, the filesystem policy should be reflected in the image, packages, service configuration, and application design. A read-only root filesystem is not a magic hardening switch. It is a product design decision that needs the rest of the system to cooperate.
Logging Policy
Development images often keep verbose logs because engineers need evidence. A production device needs a logging policy.
Think about:
- what gets logged by default
- where logs are written
- how much storage logs can consume
- whether logs survive reboot
- whether sensitive product or customer data can appear in logs
- how support engineers retrieve useful evidence
Again, this is not about randomly turning logging down. A product with poor diagnostics is miserable to support. The goal is controlled, useful logging rather than unlimited development chatter.
Common mistake: hardening the running target
One common mistake is to boot the image and then manually remove services, users, keys, packages, or configuration files until it looks more production-like.
That can make one target look better. It does not make the product reproducible.
Manual target hardening creates several problems:
- the changes may not be captured in version control
- CI will not reproduce them
- the next image build will bring the old behaviour back
- other boards may be configured differently
- nobody can easily explain which metadata created the released state
If something should not be in the production image, fix the metadata that put it
there. Remove the package from the image, change the packagegroup, adjust
PACKAGECONFIG, change service enablement, or move the development convenience
into the development image where it belongs.
What to check before release
Before treating an image as a release candidate, check:
- login configuration, including root login and empty passwords
- SSH access, keys, and authentication policy
- active services and whether they are intentionally enabled
- listening network ports
- unnecessary packages, especially development tools and test utilities
- development image features such as
debug-tweaks - whether package-management tools are present for a real product reason
- writable filesystem assumptions and persistent data locations
- debug facilities, debug symbols, and
-dbgpackages - logging behaviour and storage impact
Keep secure boot, cryptographic update verification, measured boot, TPMs, OP-TEE, SELinux, and detailed security architecture in the right course. They matter, but they are not the point of this lesson. The point here is simpler: make the production image a deliberate product artifact, not a development image with a few things removed by hand.
Check your understanding
Quick quiz: production images
Check the distinction between development convenience and production image policy.