Docker container as a Linux system service
Introduction
There are many different ways to orchestrate Docker container management, initialization, deployment, etc. Docker provides its own Compose tool and Swarm mode. There are also third-party container orchestration tools such as Kubernetes, Rancher, and Apache Mesos.
For simple deployments on a single Linux host, you might not need any of these complex solutions. Docker daemon offers simple means to start, stop, manage, and query the status of deployed containers. In this post, we'll cover how to use a Docker + systemd approach to deploy containers as Linux services without the need for third-party tools or complex deployment descriptors.
In this tutorial, we will show how to deploy Portainer CE as a Linux systemd service.

Docker restart policies
Before diving into systemd integration, it's worth mentioning that Docker has built-in restart policies that can handle container restarts automatically.
The --restart flag supports several policies:
| Policy | Description |
|---|---|
no | Do not automatically restart the container (default) |
on-failure[:max-retries] | Restart only if the container exits with a non-zero status |
always | Always restart the container regardless of the exit status |
unless-stopped | Like always, but doesn't restart containers that were explicitly stopped |
For example, to run Portainer with automatic restarts:
docker run -d --name portainer --restart unless-stopped \
-p 9443:9443 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:ltsThis approach is simpler than systemd integration and works well for most use cases. However, systemd integration offers additional benefits:
- Dependency management: Start containers after specific services (network, databases, etc.)
- Unified logging: Container logs appear in journald alongside system logs
- Consistent management: Use familiar
systemctlcommands for all services - Boot ordering: Fine-grained control over startup sequence
Existing container
The simplest method to deploy a container as a service is to create a Docker container with a given name and then map each of the Docker operations (start and stop) to systemd service commands.
docker run -d --name portainer \
-p 9443:9443 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:ltsOnce we've created this container, we can start, stop, and restart it using the regular Docker commands by indicating the container name (docker stop portainer, docker start portainer, docker restart portainer).
We can create a new systemd unit file with the service description by creating a new file in /etc/systemd/system/.
For this example, we'll create a new portainer.service file with the following contents:
[Unit]
Description=Portainer container
After=docker.service
Wants=network-online.target docker.socket
Requires=docker.socket
[Service]
TimeoutStartSec=0
Restart=always
ExecStart=/usr/bin/docker start -a portainer
ExecStop=/usr/bin/docker stop -t 10 portainer
[Install]
WantedBy=multi-user.targetThe unit file creates a new service and maps docker start and docker stop commands to the service start and stop sequences.
Key configuration explained:
- After=docker.service: Ensures this service starts only after Docker is ready
- Requires=docker.socket: Creates a hard dependency on the Docker socket
- Wants=network-online.target: Waits for network to be available (soft dependency)
- TimeoutStartSec=0: Disables startup timeout (useful if the image needs to be pulled)
- Restart=always: Automatically restart the container if it stops
After creating the unit file, reload systemd to pick up the new service:
systemctl daemon-reloadWe can now start/stop the service using the corresponding commands:
systemctl start portainer
systemctl stop portainerTo check the service status:
systemctl status portainerTo enable the service to start automatically at boot:
systemctl enable portainer.serviceCreate container if applicable on start
We can improve the previous unit file to create the container if it doesn't exist.
This approach lets you skip the manual container creation step (docker run…) and deploy containers by simply creating the service descriptor unit file.
[Unit]
Description=Portainer container
After=docker.service
Wants=network-online.target docker.socket
Requires=docker.socket
[Service]
TimeoutStartSec=0
Restart=always
ExecStartPre=-/usr/bin/docker stop %n
ExecStartPre=-/usr/bin/docker rm %n
ExecStartPre=/usr/bin/docker pull portainer/portainer-ce:lts
ExecStart=/usr/bin/docker run --rm --name %n \
-p 9443:9443 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:lts
ExecStop=/usr/bin/docker stop -t 10 %n
[Install]
WantedBy=multi-user.targetThis improved unit file uses several best practices:
- %n variable: Expands to the service name (e.g.,
portainer.service), making the unit file more portable - Dash prefix (-): The
-beforeExecStartPrecommands means "do not fail if this command fails". This is useful because the container might not exist on first run - docker pull: Ensures you always run the latest version of the image
- --rm flag: Automatically removes the container when it stops, ensuring a clean state on restart
- Foreground mode: Unlike the previous example, this runs
docker rundirectly (not in detached mode), allowing systemd to properly monitor the process
Alternative: Conditional container creation
If you prefer to keep an existing container rather than recreating it each time, use this approach:
[Unit]
Description=Portainer container
After=docker.service
Wants=network-online.target docker.socket
Requires=docker.socket
[Service]
TimeoutStartSec=0
Restart=always
ExecStartPre=/bin/bash -c "/usr/bin/docker container inspect portainer 2> /dev/null || /usr/bin/docker run -d --name portainer -p 9443:9443 -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce:lts"
ExecStart=/usr/bin/docker start -a portainer
ExecStop=/usr/bin/docker stop -t 10 portainer
[Install]
WantedBy=multi-user.targetSystemd unit files have certain limitations when executing commands in the ExecStart, ExecStop, ExecStartPre definitions.
To overcome these limitations, we encapsulate the command inside a /bin/bash -c command for more flexibility.
The first command (docker container inspect) checks if a container with this name already exists.
If it throws an error (container doesn't exist), the next instruction runs to create it.
It's important to execute the docker run command with the -d (detached) flag when using this approach.
Otherwise, the command won't return and the next step (ExecStart) won't run, preventing systemd from becoming active.
Depending on whether the container exists, the service start sequence behaves differently:
- Container already exists:
- ExecStartPre returns with no error on the first command (
docker container inspect…), so thedocker run…command doesn't execute - ExecStart runs normally and the process attaches to the container
- ExecStartPre returns with no error on the first command (
- Container doesn't exist:
- ExecStartPre returns an error on the first command, triggering the second command to create and start the container in detached mode
- ExecStart runs (
docker start) which attaches to the already-running container
Viewing container logs
One benefit of running containers through systemd is unified logging.
Container output is captured by journald, and you can view logs using the standard journalctl command:
# View all logs for the service
journalctl -u portainer.service
# Follow logs in real-time
journalctl -u portainer.service -f
# View logs since last boot
journalctl -u portainer.service -bPodman alternative
If you're looking for tighter systemd integration, consider Podman as an alternative to Docker. Podman is a daemonless container engine that integrates natively with systemd through a feature called Quadlet.
With Quadlet, you create simple declarative files (.container, .pod, .network) instead of writing systemd unit files manually.
Place them in /etc/containers/systemd/ for root containers or ~/.config/containers/systemd/ for rootless containers.
Example Quadlet file for Portainer:
[Container]
ContainerName=portainer
Image=docker.io/portainer/portainer-ce:lts
PublishPort=9443:9443
Volume=/run/podman/podman.sock:/var/run/docker.sock
Volume=portainer_data:/data
[Service]
Restart=always
[Install]
WantedBy=default.targetAfter creating this file, run systemctl daemon-reload and the container will be managed as a native systemd service.
Podman's advantages for systemd integration include:
- Daemonless architecture: No single point of failure like dockerd
- Rootless containers: Run containers without root privileges
- Native systemd integration: Quadlet generates optimized unit files automatically
- Auto-update support: Automatically pull and restart when new images are available
Conclusion
In this tutorial, I've shown several ways to deploy Docker containers as Linux systemd services:
- Docker restart policies: The simplest approach for basic restart handling
- Systemd unit files: For dependency management, unified logging, and boot ordering
- Podman with Quadlet: The most integrated solution for systemd-based container management
The Docker + systemd approach works well for:
- Single-host deployments that don't need full orchestration
- Servers where you want containers managed alongside other systemd services
- Environments with complex service dependencies
- Teams familiar with systemd who want consistent management across all services
For production deployments at scale, consider Kubernetes or Docker Swarm. For simpler single-host scenarios, the approaches described in this post provide a lightweight alternative that leverages your existing Linux knowledge.

Comments in "Docker container as a Linux system service"
I am just setting up a simple DNS server for a home lab and I wanted to do it with CoreDNS in a docker container. This was a great help.
Having a little trouble getting the container to stop but I will try to figure that out. I am also going to work on having a template file so I can set up multiple instances as detailed at https://coreos.com/os/docs/latest/using-environment-variables-in-systemd-units.html.
Cheers again,
Ed.