civil-and-structural-engineering
Deploying Docker Containers with Systemd for Automated Startup
Table of Contents
Why Combine Systemd with Docker for Production Deployments
Modern infrastructure demands that containerized services survive unexpected reboots, hardware failures, or package updates. While Docker provides restart policies (--restart always), these policies only work as long as the Docker daemon is running. Systemd – the init system used by Ubuntu, Debian, Fedora, CentOS, and most modern Linux distributions – takes this further by managing the lifecycle of the Docker daemon itself and can start containers even before the Docker socket becomes available. Benefits of using systemd include:
- Guaranteed startup order through dependency directives (e.g., after network.target, after docker.service)
- Unified logging via
journalctl, making debugging straightforward - Fine-grained control over resource limits (CPU, memory, I/O) using systemd unit directives
- Automatic restart on failure with configurable delay and burst limits
- Support for socket activation and timed startup
By wrapping each Docker container in a systemd service file, operations teams gain a consistent interface for starting, stopping, and monitoring containers, reducing reliance on ad-hoc scripts and manual intervention.
Creating a Systemd Service for a Single Docker Container
The standard approach involves writing a service unit file that calls Docker commands to run and stop the container. Below we walk through the process step by step, starting with a basic example and then covering common production requirements.
Step 1: Write the Service Unit File
Create a file named /etc/systemd/system/myapp.service. Use the following template as a starting point:
[Unit]
Description=My Application Container
After=network-online.target docker.service
Wants=network-online.target
Requires=docker.service
[Service]
Restart=always
RestartSec=10
StartLimitBurst=3
ExecStartPre=-/usr/bin/docker kill myapp
ExecStartPre=-/usr/bin/docker rm myapp
ExecStart=/usr/bin/docker run --rm --name myapp \
-e DB_HOST=10.0.1.50 \
-e DB_PORT=5432 \
-v /data/myapp:/app/data \
-p 8080:8080 \
myregistry/myapp:latest
ExecStop=/usr/bin/docker stop -t 10 myapp
ExecStopPost=-/usr/bin/docker rm myapp
[Install]
WantedBy=multi-user.target
Explanation of key directives:
After=docker.service– ensures Docker daemon is running before starting the container.Requires=docker.service– if Docker is stopped, this service stops as well.ExecStartPre– cleans up any leftover container from a previous run (the-prefix means failures here are non-fatal).ExecStart– usesdocker run --rmto automatically remove the container when it stops.ExecStop– gracefully stops the container with a timeout (10 seconds).Restart=always– restarts the container regardless of exit code.RestartSec– waits 10 seconds before restarting.StartLimitBurst– limits restarts to 3 attempts per interval (default 10 seconds) to avoid restart loops.
Step 2: Enable and Start the Service
sudo systemctl daemon-reload
sudo systemctl enable myapp.service
sudo systemctl start myapp.service
The daemon-reload tells systemd to re-read service files. enable creates the symlink so the service starts on boot.
Managing the Service with Standard Systemd Commands
Once the service is running, you control it just like any other system service:
- Start:
sudo systemctl start myapp.service - Stop:
sudo systemctl stop myapp.service - Restart:
sudo systemctl restart myapp.service - Status:
sudo systemctl status myapp.service - Logs:
sudo journalctl -u myapp.service -f(follow live logs)
Advanced Configuration Patterns
Production deployments often require more than a simple docker run. Below are common enhancements you can add to your systemd service files.
Passing Environment Variables
Hard-coding secrets or configuration in the service file is not recommended. Instead, use a separate environment file:
[Service]
EnvironmentFile=-/etc/myapp/env.conf
ExecStart=/usr/bin/docker run --rm --name myapp \
--env-file /etc/myapp/env.conf \
myregistry/myapp:latest
The - prefix before the path means the service will start even if the file doesn't exist (useful during initial setup).
Networking and Port Bindings
For containers that need to communicate with each other on the same host, consider using --network host or user-defined bridge networks. Example:
ExecStart=/usr/bin/docker run --rm --name web \
--network=my-net \
-p 443:443 \
-v /etc/ssl/certs:/etc/ssl/certs:ro \
myregistry/web:latest
If using a custom network, ensure the network exists before the service starts. You can add an ExecStartPre command to create it:
ExecStartPre=/usr/bin/docker network create my-net
Inter-Container Dependencies
When one container requires another to be ready before starting (e.g., a web app waiting for a database), systemd can enforce ordering. Create a second service file for the database and then:
[Unit]
Description=Web App Container
After=network-online.target docker.service mydb.service
BindsTo=mydb.service
BindsTo ties the web app’s lifecycle to the database container – if the database stops, the web app is also stopped.
Health Checks and Readiness
Docker health checks can be integrated with systemd to prevent premature service availability. Use ExecStartPost with a script that polls the health endpoint:
ExecStartPost=/usr/local/bin/wait-for-health.sh http://localhost:8080/health 30
The script should exit 0 only when the container is healthy. If it fails, systemd marks the unit as failed.
Resource Limits via Systemd
You can constrain a container’s CPU and memory at the cgroup level without Docker’s own resource flags. This is especially useful when running multiple containers on a single host:
[Service]
MemoryMax=512M
CPUQuota=50%
These settings create a hard limit that systemd enforces independently of Docker.
Managing Multiple Containers: Systemd vs. Docker Compose
For a small number of containers (e.g., 2-5), individual systemd service files are simple and maintainable. However, when a project involves many interconnected services, Docker Compose becomes more convenient. You can still use systemd to orchestrate the entire Docker Compose stack by creating a single service unit that calls docker-compose up. Example:
[Unit]
Description=My Application Stack
After=network-online.target docker.service
Requires=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/myapp
ExecStart=/usr/local/bin/docker-compose up -d
ExecStop=/usr/local/bin/docker-compose down
[Install]
WantedBy=multi-user.target
This approach gives you the simplicity of Compose for defining services combined with systemd’s lifecycle management. Note that Type=oneshot is used because docker-compose up -d exits immediately. RemainAfterExit=yes keeps the unit in an “active” state until ExecStop is called.
Which method should you choose?
- Individual systemd services – best for legacy applications, services with strict startup ordering, or when you need per-container resource limits.
- Docker Compose with systemd – ideal for microservices stacks where dependencies are handled internally by Compose, and you want a single unit to manage the whole group.
Troubleshooting Common Issues
Even with careful setup, you may encounter problems. Below are frequent pitfalls and their solutions.
Service Fails with “Cannot connect to the Docker daemon”
This usually means the service starts before the Docker socket is ready. Ensure your unit contains After=docker.service and Requires=docker.service. Also check that the Docker daemon is enabled: sudo systemctl enable docker.
Container Restarts in a Loop
If the container exits immediately, systemd will keep restarting it according to RestartSec and StartLimitBurst. Check container logs with docker logs mycontainer. Increase RestartSec (e.g., 30 seconds) and set StartLimitBurst=3 to prevent a busy loop.
Service Does Not Stop Cleanly
An incorrectly configured ExecStop may leave the container running. Verify that docker stop -t 10 mycontainer uses the correct container name. Use ExecStopPost to forcefully remove the container if the stop fails.
Environment Variables Not Loaded
If you use EnvironmentFile, confirm the file exists and is readable by root. Avoid quoting issues – systemd strips quotes from variable values. For secret injection, consider using systemd credentials or a dedicated secret manager.
Security Considerations
Running Docker containers through systemd raises a few security points:
- Always run the systemd service as a non-root user if possible (use
User=andGroup=directives, but ensure the user has access to the Docker socket or run in rootless mode). - Avoid using
--privilegedin systemd units unless absolutely necessary. - Use read-only bind mounts (
:ro) whenever the container does not need to write to the host. - Leverage systemd’s
ProtectSystem=strictandPrivateTmp=trueto harden the unit against escapes.
[Service]
ProtectSystem=strict
ReadWritePaths=/var/log/myapp
PrivateTmp=true
User=myappuser
External Resources
For further reading, consult these official references:
Conclusion
Integrating systemd with Docker containers gives you a robust, automated startup mechanism that integrates seamlessly with the rest of your Linux system. By writing well-structured service unit files, you can control startup order, manage dependencies, set resource limits, and monitor logs using tools your operations team already knows. Whether you choose individual services for each container or a single unit to orchestrate a Compose stack, systemd provides the reliability and predictability that production environments demand.
Start with a simple unit file, test it thoroughly, then layer on advanced options like environment files, health checks, and security hardening. With this approach, your Docker containers will survive reboots, crashes, and configuration changes without manual intervention, freeing your team to focus on building applications.