You have a lightweight container meant to run a periodic cleanup script or report generator. You baked cron into the Dockerfile, installed the service, and configured a crontab. It “works” in dev, but in production, the container stays alive forever, consuming memory, holding open file descriptors, and complicating health checks. Worse, when the host reboots, the cron daemon inside the container doesn’t auto-start unless you force it, and logs get buried in the container’s stdout stream instead of your centralized syslog.
The 5-minute setup
Remove the cron daemon from your container image entirely. Instead, use a minimal base image (like alpine or distroless) that runs your script and exits. Then, schedule the container execution from the host using docker run with --rm and --network none (if no network is needed) via a standard systemd timer or the host’s crontab.
Here is a concrete example using a host-side systemd timer to run a Python script that cleans up old temporary files in a shared volume.
Dockerfile):FROM python:3.11-slim
WORKDIR /app
COPY cleanup_script.py .
# No CMD/ENTRYPOINT that blocks. The script exits after completion.
CMD ["python", "cleanup_script.py"]
docker build -t my-cleanup-tool:latest .
/etc/systemd/system/cleanup-task.service):[Unit]
Description=Run containerized cleanup task
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/bin/docker run --rm --name cleanup-task-container \
--volume /var/data/shared:/app/shared:ro \
--user 1000:1000 \
my-cleanup-tool:latest
# Important: Do not restart on failure. One-shot means one run.
Restart=no
/etc/systemd/system/cleanup-task.timer):[Unit]
Description=Run cleanup task daily at 03:00
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now cleanup-task.timer
Why it works
Containers are ephemeral by design. Keeping a long-running daemon inside a container defeats the purpose of containerization and introduces stateful complexity. By making the container a short-lived job that exits upon completion, you align with the Unix philosophy and Docker best practices. The host OS (systemd or cron) handles scheduling, logging, and failure alerts natively. The --rm flag ensures no dead containers pile up, and Type=oneshot tells systemd to consider the service successful only when the process exits cleanly, enabling accurate exit-code monitoring.
Pro Tip
Add --network none and --cap-drop ALL to your docker run command in the systemd service to harden the container against network-based attacks and privilege escalation. For example:
ExecStart=/usr/bin/docker run --rm --network none --cap-drop ALL \
--security-opt no-new-privileges:true \
my-cleanup-tool:latest
This ensures the cleanup script cannot accidentally initiate external connections or gain elevated privileges, even if the script has a bug or is compromised.