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

ParameterDescription
nameUnit name
statestarted, stopped, restarted, reloaded
enabledStart on boot (true/false)
daemon_reloadReload systemd manager config
maskedMask unit (prevent starting)
scopesystem, user, global
no_blockDon'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

OnCalendarSchedule
*-*-* 02:00:00Daily at 2 AM
Mon *-*-* 09:00:00Monday 9 AM
*-*-01 00:00:00First of each month
hourlyEvery hour
dailyEvery day
weeklyEvery week
*-*-* *:00/15:00Every 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

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.