Introduction
systemd manages services on modern Linux distributions. Ansible's systemd_service module controls units — start, stop, enable, mask, and reload. Combined with templates, you can deploy custom services, timers, and socket-activated units.
Basic Service Management
# Start and enable service
- name: Start nginx
ansible.builtin.systemd_service:
name: nginx
state: started
enabled: true
# Stop service
- name: Stop nginx
ansible.builtin.systemd_service:
name: nginx
state: stopped
# Restart service
- name: Restart nginx
ansible.builtin.systemd_service:
name: nginx
state: restarted
# Reload (graceful)
- name: Reload nginx
ansible.builtin.systemd_service:
name: nginx
state: reloaded
# Daemon reload (after unit file changes)
- name: Reload systemd
ansible.builtin.systemd_service:
daemon_reload: true
Parameters
| Parameter | Description |
|---|---|
name | Unit name |
state | started, stopped, restarted, reloaded |
enabled | Start on boot (true/false) |
daemon_reload | Reload systemd manager config |
masked | Mask unit (prevent starting) |
scope | system, user, global |
no_block | Don't wait for completion |
Create Custom Service
# Deploy service unit file
- name: Deploy myapp service
ansible.builtin.template:
src: myapp.service.j2
dest: /etc/systemd/system/myapp.service
owner: root
mode: "0644"
notify:
- Reload systemd
- Restart myapp
# Start and enable
- name: Enable myapp
ansible.builtin.systemd_service:
name: myapp
state: started
enabled: true
handlers:
- name: Reload systemd
ansible.builtin.systemd_service:
daemon_reload: true
- name: Restart myapp
ansible.builtin.systemd_service:
name: myapp
state: restarted
Service Unit Template
# templates/myapp.service.j2
[Unit]
Description={{ app_description | default('My Application') }}
After=network.target
{% if app_requires_db | default(false) %}
After=postgresql.service
Requires=postgresql.service
{% endif %}
[Service]
Type=simple
User={{ app_user }}
Group={{ app_group | default(app_user) }}
WorkingDirectory={{ app_dir }}
ExecStart={{ app_dir }}/bin/{{ app_name }} --port {{ app_port }}
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier={{ app_name }}
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths={{ app_dir }}/data {{ app_dir }}/logs
PrivateTmp=true
# Environment
{% for key, value in (app_env | default({})).items() %}
Environment="{{ key }}={{ value }}"
{% endfor %}
{% if app_env_file is defined %}
EnvironmentFile={{ app_env_file }}
{% endif %}
# Resource limits
{% if app_memory_limit is defined %}
MemoryMax={{ app_memory_limit }}
{% endif %}
{% if app_cpu_quota is defined %}
CPUQuota={{ app_cpu_quota }}
{% endif %}
[Install]
WantedBy=multi-user.target
Systemd Timers (Cron Replacement)
# Deploy timer + service pair
- name: Deploy backup timer
ansible.builtin.copy:
content: |
[Unit]
Description=Daily Backup Timer
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
dest: /etc/systemd/system/backup.timer
notify: Reload systemd
- name: Deploy backup service
ansible.builtin.copy:
content: |
[Unit]
Description=Backup Service
[Service]
Type=oneshot
ExecStart=/opt/scripts/backup.sh
User=backup
dest: /etc/systemd/system/backup.service
notify: Reload systemd
- name: Enable backup timer
ansible.builtin.systemd_service:
name: backup.timer
state: started
enabled: true
Timer Schedules
| OnCalendar | Schedule |
|---|---|
*-*-* 02:00:00 | Daily at 2 AM |
Mon *-*-* 09:00:00 | Monday 9 AM |
*-*-01 00:00:00 | First of each month |
hourly | Every hour |
daily | Every day |
weekly | Every week |
*-*-* *:00/15:00 | Every 15 minutes |
Mask/Unmask Services
# Prevent a service from being started (even manually)
- name: Mask bluetooth
ansible.builtin.systemd_service:
name: bluetooth
masked: true
# Unmask
- name: Unmask bluetooth
ansible.builtin.systemd_service:
name: bluetooth
masked: false
Socket Activation
# Deploy socket-activated service
- name: Deploy socket
ansible.builtin.copy:
content: |
[Unit]
Description=MyApp Socket
[Socket]
ListenStream={{ app_port }}
Accept=false
[Install]
WantedBy=sockets.target
dest: /etc/systemd/system/myapp.socket
notify: Reload systemd
- name: Enable socket
ansible.builtin.systemd_service:
name: myapp.socket
state: started
enabled: true
Practical Pattern: Java Application
- name: Deploy Java app service
ansible.builtin.template:
src: java-app.service.j2
dest: /etc/systemd/system/{{ app_name }}.service
vars:
java_opts: "-Xms512m -Xmx2g -XX:+UseG1GC"
notify:
- Reload systemd
- Restart app
# templates/java-app.service.j2
[Unit]
Description={{ app_name }}
After=network.target
[Service]
Type=simple
User={{ app_user }}
ExecStart=/usr/bin/java {{ java_opts }} -jar {{ app_dir }}/{{ app_name }}.jar
Restart=always
RestartSec=10
SuccessExitStatus=143
Environment="JAVA_OPTS={{ java_opts }}"
Environment="SPRING_PROFILES_ACTIVE={{ app_env }}"
[Install]
WantedBy=multi-user.target
Troubleshooting
Service Won't Start
- name: Check service status
ansible.builtin.command: systemctl status myapp
register: status
changed_when: false
failed_when: false
- name: Check journal logs
ansible.builtin.command: journalctl -u myapp -n 50 --no-pager
register: logs
changed_when: false
- debug:
msg: "{{ logs.stdout_lines }}"
"Unit file changed on disk"
Always reload after changing unit files:
- name: Deploy unit file
template: ...
notify: Reload systemd # IMPORTANT!
handlers:
- name: Reload systemd
systemd_service:
daemon_reload: true
Related Articles
Conclusion
systemd is the backbone of modern Linux service management. Use ansible.builtin.systemd_service for start/stop/enable operations. Deploy custom unit files with templates for full control over service behavior. Use security directives (NoNewPrivileges, ProtectSystem, PrivateTmp) to harden services. Replace cron with systemd timers for better logging and control. Always daemon_reload after changing unit files. Use handlers to chain reload → restart on config changes.