Abstract: This article documents the exploration of achieving service persistence for Podman in rootless mode. Starting with a common failure case of manually creating a systemd service, it analyzes the root cause and ultimately transitions to using Quadlet, the officially recommended tool from Podman. Through a deep dive into Quadlet’s mechanics, this article explains its declarative service management approach and presents the resulting best practices.
1. The Goal and the Failure of the Initial Attempt#
After migrating from Docker to Podman, one of the core goals was to achieve persistence and auto-start for containerized services.
Initially, I attempted to write a traditional systemd user service for a podman-compose project. However, this approach failed because systemd’s lifecycle management for background processes (like podman-compose up -d) was incompatible with expectations, leading the service into a “start-stop” infinite loop. This prompted me to turn to Podman’s official solution: Quadlet.
2. Initial Quadlet Exploration and a New Confusion#
The core idea behind Quadlet is that a user only needs to write a simple .container file, which is then automatically “translated” into a complex .service file by a systemd generator.
Following the compose.yml file, I created a corresponding Quadlet file ~/.config/containers/systemd/sing-box.container for the sing-box container, ensuring it included an [Install] section to define its auto-start behavior.
However, after running systemctl --user daemon-reload, my attempt to use systemctl --user enable sing-box.service was repeatedly met with the error: Failed to enable unit: Unit ... is transient or generated. Interestingly, systemctl --user start sing-box.service was able to start the container successfully.
This indicated that the Quadlet file itself was valid, but there was an interaction between it and systemd’s enable mechanism that was beyond conventional understanding.
3. Root Cause Analysis: The Inner Workings of the Quadlet Generator and [Install]#
Through research and repeated experimentation, the root of the problem became clear: Quadlet’s autostart capability is not granted by the systemctl enable command but is automatically handled when its generator reads the [Install] section in the .container file.
The systemd workflow is as follows:
Write: The user creates a
.containerfile with an[Install]section in the specified directory¹. This is the “autostart directive” given tosystemd.daemon-reloadtriggers: Whensystemctl daemon-reload² is executed, thequadlet-generatoris activated. It scans the directory, finds the.containerfile, and performs two key actions:- Generates a transient service file: In a temporary runtime directory (like
/run/systemd/generator/), it creates a.servicefile that does not include the[Install]section. This file is used for the actualstartandstopoperations. - Automatically creates symbolic links: It reads the
[Install]section from the “blueprint” and directly performs the core task ofenableon its behalf—creating symbolic links from the appropriate.wants/directory (based onWantedBy) to the transient.servicefile.
- Generates a transient service file: In a temporary runtime directory (like
The
enablecommand issue: After this, when the user manually runssystemctl enable,systemdsees that the service has already been “installed” by a generator and points to a transient file. According to its design principles, a user should not directlyenablea transient unit managed by a generator, so it returns theUnit is transient or generatederror. This error is actually a hint: “I’ve already taken care of this for you.”
¹ Quadlet File Paths: For user services (Rootless), the path is ~/.config/containers/systemd/. For system services (Rootful), the path is /etc/containers/systemd/.
² Command Note: For user services, the command is systemctl --user daemon-reload. For system services, it is sudo systemctl daemon-reload.
4. [Install] Configuration and the Final Workflow#
4.1. Correctly Configuring the [Install] Section#
The value of WantedBy= depends on the mode you are running Podman in:
Rootless Mode: The service belongs to the user session and should start with the user session (or with the system if
lingeris enabled).1[Install] 2WantedBy=default.targetRootful Mode: The service belongs to the system and should start when the system enters the multi-user state.
1[Install] 2WantedBy=multi-user.target
4.2. The Final, Simplified Workflow#
Write or Modify the Quadlet source file: Ensure the file syntax is correct and includes the appropriate
[Install]section for your runtime mode.Apply Changes: After every modification to the
.containerfile, execute the following command to notifysystemdand trigger the generator. This is the only necessary management command.1# For user services 2systemctl --user daemon-reload 3# For system services 4sudo systemctl daemon-reloadStart and Verify: Once
daemon-reloadis complete, the service is already in an “installed” and “enabled” state.1# Start the service (use sudo and/or --user depending on the mode) 2systemctl --user start your-service.service 3 4# Verify that it is enabled 5systemctl --user is-enabled your-service.service 6# Expected output: generated
5. Summary#
This troubleshooting journey, which started from a seemingly simple need for service persistence, led to a deep dive into the complex interaction between systemd and the Quadlet generator. It revealed the powerful automation behind declarative tools and clarified the changing role of traditional systemctl commands when interacting with generators.
The final conclusion is that Quadlet achieves highly automated and declarative persistence management by reading the [Install] configuration in the .container file and automatically completing the service “installation” (the core work of enable) during a daemon-reload. Understanding the difference in [Install] configuration between rootful and rootless modes and following the “edit source file -> daemon-reload -> start” workflow is key to mastering Podman container deployment.
