Introduction

Managing Docker containers as system services on CentOS and RHEL systems offers a robust, native approach to container orchestration. By integrating containers into systemd, you gain automatic startup on boot, integrated logging via journald, and straightforward lifecycle management. This article provides a comprehensive guide to deploying and managing Docker containers with systemd, including best practices, advanced configuration, and troubleshooting techniques.

Systemd is the default init system on modern CentOS and RHEL distributions (7 and later). It provides a dependency-based service manager that can handle Docker containers just like any other system service. This approach is particularly useful for production environments where reliability, monitoring, and consistent restart behavior are critical.

Prerequisites

  • A CentOS or RHEL system (version 7 or later) with root or sudo access.
  • Docker Engine installed and the Docker daemon running.
  • Familiarity with basic command-line operations, systemd units, and Docker commands.
  • Network connectivity to pull container images from a registry (e.g., Docker Hub).

Installing Docker on CentOS and RHEL

If Docker is not already present, install it using the official repository. The steps below work for CentOS 7/8 and RHEL 7/8 (for RHEL 9, adjust the repository URL accordingly).

Step 1: Install Required Packages

First, install yum-utils to manage repositories:

sudo yum install -y yum-utils

Step 2: Add the Docker Repository

Add the official Docker CE repository:

sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

For RHEL systems, you may need to enable the rhel-7-server-extras-rpms or rhel-8-for-x86_64-baseos-rpms repository depending on your subscription. Alternatively, you can use the CentOS repository as shown above, but verify compatibility.

Step 3: Install Docker Engine

sudo yum install -y docker-ce docker-ce-cli containerd.io

Step 4: Start and Enable Docker

sudo systemctl start docker
sudo systemctl enable docker

Verify the installation:

sudo docker run hello-world

For more details, refer to the official Docker installation guide for CentOS.

Creating a Systemd Service for a Docker Container

Each container managed by systemd requires a dedicated unit file. This file defines how the container is started, stopped, and restarted. Below we create a service for an Nginx container.

Step 1: Create the Unit File

Create a file named /etc/systemd/system/nginx-docker.service:

sudo nano /etc/systemd/system/nginx-docker.service

Step 2: Define the Unit

Add the following content:

[Unit]
Description=Nginx Docker Container
After=docker.service
Requires=docker.service

[Service]
Restart=always
ExecStart=/usr/bin/docker run --rm --name nginx-container -p 80:80 nginx:latest
ExecStop=/usr/bin/docker stop nginx-container
ExecStartPre=-/usr/bin/docker rm nginx-container

[Install]
WantedBy=multi-user.target

Explanation of the unit file:

  • [Unit]: Describes the service and declares dependencies. After=docker.service ensures Docker is running before the container starts. Requires=docker.service makes Docker a hard dependency.
  • [Service]: Defines the runtime behavior. Restart=always auto-restarts the container on exit (e.g., crashes). ExecStart launches the container; --rm removes the container after it stops (useful for ephemeral containers). ExecStop sends a graceful stop to the container. ExecStartPre cleans up any leftover container with the same name before starting.
  • [Install]: Sets the target that enables the service to start at boot. multi-user.target is the standard runlevel for non-graphical systems.

Note the use of the full path to the docker binary. Always specify absolute paths in systemd units to avoid PATH-related issues.

Step 3: Reload systemd and Start the Service

sudo systemctl daemon-reload
sudo systemctl enable nginx-docker
sudo systemctl start nginx-docker

Check the status:

sudo systemctl status nginx-docker

Managing the Service

Once the unit is active, you can control the container like any other systemd service:

  • Start/Stop/Restart: sudo systemctl start|stop|restart nginx-docker
  • Enable/Disable on boot: sudo systemctl enable|disable nginx-docker
  • View logs: sudo journalctl -u nginx-docker
  • Follow logs in real time: sudo journalctl -u nginx-docker -f

If you need to update the container image, stop the service, modify the ExecStart line to use a new image tag, reload the daemon, and start again. For zero-downtime updates, consider using a reverse proxy like Nginx with dynamic upstreams.

Advanced Configuration

The basic unit file can be extended to meet production requirements. Below are several advanced patterns.

Restart Policies and Backoff

Systemd’s Restart=always can cause a restart loop if a container fails immediately. To mitigate this, add RestartSec=5 (or higher) to introduce a delay between restarts:

Restart=always
RestartSec=5

For containers that should restart only on failure (not on clean exit), use Restart=on-failure. You can also set StartLimitIntervalSec=600 and StartLimitBurst=5 to prevent excessive restarts.

Resource Limits via systemd

Systemd can enforce CPU and memory limits using cgroups. Add the following to the [Service] section:

MemoryMax=512M
CPUQuota=50%

These settings limit the container to 512 MB of memory and 50% of a single CPU core. This is often simpler than using Docker’s native resource constraints (--memory, --cpus) and ensures limits are applied even if the container is restarted outside Docker.

Environment Variables

Pass environment variables to the container using Environment or EnvironmentFile:

Environment="MY_VAR=value"
EnvironmentFile=-/etc/containers/nginx.env

The leading - in EnvironmentFile means the file is optional; systemd will not fail if it is missing. This is useful for environment-specific configurations.

Logging and Journald Integration

By default, systemd captures stdout/stderr from the container and stores them in the journal. You can adjust log handling with StandardOutput and StandardError directives. For example, to log to a file instead:

StandardOutput=file:/var/log/nginx-container.log
StandardError=file:/var/log/nginx-container-error.log

Be cautious when using file logging; journald already provides robust log rotation via journalctl. For long-running containers, consider setting LogRateLimitIntervalSec=30 to prevent log flooding.

Health Checks

While systemd does not have built-in health check functionality, you can combine ExecStartPost with a script that verifies the container is responsive. For instance:

ExecStartPost=/usr/local/bin/wait-for-it.sh localhost:80 --timeout=30

If the script fails (non-zero exit), systemd marks the service as failed. For more sophisticated health monitoring, use Docker’s built-in health check inside the container and parse it with a custom systemd ExecStopPost script.

Monitoring and Troubleshooting

Checking Service Status

Use systemctl status to see whether the container is running, how long it has been up, and recent log lines:

sudo systemctl status nginx-docker

Look for the Active line. A healthy service shows active (running).

Viewing Container Logs

Journald stores logs with the unit name. To see all logs:

sudo journalctl -u nginx-docker

To see logs from the last hour:

sudo journalctl -u nginx-docker --since "1 hour ago"

If the container uses Docker’s logging driver (e.g., json-file), you can also view logs with docker logs nginx-container. However, journald integration is preferred for consistency with system services.

Common Issues and Solutions

  • Service fails to start, “Container is already in use”: Add ExecStartPre=-/usr/bin/docker rm nginx-container (note the leading - to ignore errors if the container does not exist).
  • Docker daemon not ready when systemd tries to start the container: Ensure After=docker.service and Requires=docker.service are set. You might also add BindsTo=docker.service to stop the container if Docker stops.
  • Permission denied for Docker commands: The systemd service runs as root by default. If you change the user with User=, that user must be in the docker group or have equivalent socket permissions. For security, avoid running the service as a non-root user unless you have configured rootless Docker.
  • High disk usage from journal logs: Configure log rotation in /etc/systemd/journald.conf (e.g., SystemMaxUse=500M).

Comparison with Docker-Compose and Systemd Slices

For multi-container deployments, Docker Compose is often more convenient, but it does not integrate as tightly with systemd. A common pattern is to create a systemd service that runs docker-compose up and another that runs docker-compose down. Alternatively, you can create individual systemd services for each container and group them using systemctl enable --now with a target.

For advanced resource isolation, systemd slices can group related services (e.g., service.slice) and apply unified resource limits. Example:

sudo systemctl set-property nginx-docker.service MemoryMax=256M

This applies the limit at runtime without editing the unit file.

Security Considerations

Running containers with systemd does not inherently compromise security, but you should follow best practices:

  • Use read-only root filesystems when possible: add --read-only to the docker run command.
  • Drop Linux capabilities: --cap-drop=ALL --cap-add=NET_BIND_SERVICE for the Nginx example.
  • Run containers as a non-root user inside the container (e.g., --user nginx).
  • Use systemd’s PrivateTmp=true, NoNewPrivileges=true, and ProtectSystem=strict in the service unit to harden the service itself (though these affect the Docker CLI process, not the container).

For more details, see Docker security best practices and the systemd.exec man page.

Conclusion

Deploying Docker containers with systemd on CentOS and RHEL systems provides a reliable, well-integrated method for managing containerized applications as native services. By creating custom unit files, you can control container lifecycle, enforce resource limits, and leverage systemd’s logging and monitoring capabilities. The patterns presented in this article are production-ready and can be adapted to virtually any container workload. Start by creating simple unit files for your most critical containers, then expand with advanced configurations as your infrastructure grows.

For further reading, consult the systemd.service documentation and the Docker restart policies guide.