Introduction

ansible.builtin.lineinfile manages individual lines in text files — add a line, replace a line matching a pattern, or remove lines. It's the go-to module for small, targeted file edits without rewriting the entire file with template or copy.

Parameters

ParameterDefaultDescription
path(required)File to modify
line—Line content to add/ensure
regexp—Regex to find existing line
statepresentpresent to add/ensure, absent to remove
insertafterEOFInsert after matching line/EOF/BOF
insertbefore—Insert before matching line/BOF
backrefsfalseUse regex backreferences in line
createfalseCreate file if it doesn't exist
backupfalseCreate backup before editing
mode—File permissions
owner—File owner
validate—Validation command
firstmatchfalseReplace only the first match

Add a Line

# Add line at end of file (default)
- name: Add DNS server
  ansible.builtin.lineinfile:
    path: /etc/resolv.conf
    line: "nameserver 8.8.8.8"

# Add line and create file if missing
- name: Ensure config exists
  ansible.builtin.lineinfile:
    path: /etc/myapp/config
    line: "max_connections=100"
    create: true
    mode: "0644"

Ensure a Line Exists (Idempotent)

# If line exists → no change. If missing → adds it.
- name: Ensure NOPASSWD for deploy user
  ansible.builtin.lineinfile:
    path: /etc/sudoers
    line: "deploy ALL=(ALL) NOPASSWD: ALL"
    validate: "visudo -cf %s"

# Ensure SSH setting
- name: Disable root login
  ansible.builtin.lineinfile:
    path: /etc/ssh/sshd_config
    regexp: "^#?PermitRootLogin"    # Match commented or uncommented
    line: "PermitRootLogin no"
  notify: Restart sshd

Replace a Line (regexp + line)

When you specify both regexp and line:

  • If a line matches regexp → replace it with line
  • If no line matches → add line at end of file
# Change SSH port
- name: Set SSH port
  ansible.builtin.lineinfile:
    path: /etc/ssh/sshd_config
    regexp: "^#?Port "
    line: "Port 2222"
  notify: Restart sshd

# Change timezone
- name: Set timezone
  ansible.builtin.lineinfile:
    path: /etc/environment
    regexp: "^TZ="
    line: "TZ=UTC"

# Update config value
- name: Set max connections
  ansible.builtin.lineinfile:
    path: /etc/postgresql/16/main/postgresql.conf
    regexp: "^#?max_connections"
    line: "max_connections = 200"
  notify: Restart postgresql

Remove a Line

# Remove by exact line
- name: Remove old DNS
  ansible.builtin.lineinfile:
    path: /etc/resolv.conf
    line: "nameserver 10.0.0.1"
    state: absent

# Remove by pattern
- name: Remove all comments starting with #TEMP
  ansible.builtin.lineinfile:
    path: /etc/myapp.conf
    regexp: "^#TEMP"
    state: absent

Insert After/Before

# Insert after a specific line
- name: Add setting after [mysqld] section
  ansible.builtin.lineinfile:
    path: /etc/mysql/my.cnf
    insertafter: "^\\[mysqld\\]"
    line: "max_connections = 500"

# Insert before a specific line
- name: Add comment before setting
  ansible.builtin.lineinfile:
    path: /etc/nginx/nginx.conf
    insertbefore: "^http {"
    line: "# Managed by Ansible"

# Insert at beginning of file
- name: Add shebang
  ansible.builtin.lineinfile:
    path: /opt/scripts/deploy.sh
    insertbefore: BOF
    line: "#!/bin/bash"

Backreferences

Capture groups from regexp and use them in line:

# Change a value while keeping the key
- name: Update listen port
  ansible.builtin.lineinfile:
    path: /etc/nginx/nginx.conf
    regexp: "^(\\s*listen\\s+)\\d+;"
    line: "\\1{{ nginx_port }};"
    backrefs: true

# Uncomment a line (remove leading #)
- name: Enable setting
  ansible.builtin.lineinfile:
    path: /etc/ssh/sshd_config
    regexp: "^#(PasswordAuthentication.*)"
    line: "\\1"
    backrefs: true

⚠️ With backrefs: true, if the regex doesn't match, the line is NOT added (unlike default behavior).

Practical Examples

SSH Hardening

---
- name: Harden SSH config
  hosts: all
  become: true
  tasks:
    - name: Disable root login
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        regexp: "^#?PermitRootLogin"
        line: "PermitRootLogin no"

    - name: Disable password auth
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        regexp: "^#?PasswordAuthentication"
        line: "PasswordAuthentication no"

    - name: Set max auth tries
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        regexp: "^#?MaxAuthTries"
        line: "MaxAuthTries 3"

    - name: Disable empty passwords
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        regexp: "^#?PermitEmptyPasswords"
        line: "PermitEmptyPasswords no"
      notify: Restart sshd

  handlers:
    - name: Restart sshd
      ansible.builtin.service:
        name: sshd
        state: restarted

Manage /etc/hosts

- name: Add host entries
  ansible.builtin.lineinfile:
    path: /etc/hosts
    regexp: ".*{{ item.name }}$"
    line: "{{ item.ip }} {{ item.name }}"
  loop:
    - { ip: "10.0.1.10", name: "db-master" }
    - { ip: "10.0.1.11", name: "db-replica" }
    - { ip: "10.0.1.20", name: "cache-01" }

Environment Variables

- name: Set environment variables
  ansible.builtin.lineinfile:
    path: /etc/environment
    regexp: "^{{ item.key }}="
    line: "{{ item.key }}={{ item.value }}"
  loop:
    - { key: LANG, value: "en_US.UTF-8" }
    - { key: APP_ENV, value: production }
    - { key: JAVA_HOME, value: /usr/lib/jvm/java-17 }

Kernel Parameters (sysctl.conf)

- name: Set kernel parameters
  ansible.builtin.lineinfile:
    path: /etc/sysctl.conf
    regexp: "^#?{{ item.key }}"
    line: "{{ item.key }} = {{ item.value }}"
  loop:
    - { key: "net.ipv4.ip_forward", value: "1" }
    - { key: "vm.swappiness", value: "10" }
    - { key: "net.core.somaxconn", value: "65535" }
  notify: Reload sysctl

lineinfile vs replace vs template

ModuleUse When
lineinfileManaging one line (add/replace/remove)
replaceReplacing text within a line or multiple lines matching a pattern
templateManaging the entire file with Jinja2
blockinfileManaging a block of lines (multi-line insert)
# lineinfile — one line
- lineinfile:
    path: /etc/config
    regexp: "^port="
    line: "port=8080"

# replace — part of a line or multiple matches
- replace:
    path: /etc/config
    regexp: "port=\\d+"
    replace: "port=8080"

# blockinfile — multiple lines
- blockinfile:
    path: /etc/config
    block: |
      # App settings
      port=8080
      host=0.0.0.0

# template — entire file
- template:
    src: config.j2
    dest: /etc/config

Troubleshooting

Line Added Multiple Times

Your regexp doesn't match the line you're adding. The module checks regexp for existing matches — if it doesn't find one, it adds line:

# WRONG — regexp won't match the line being added
- lineinfile:
    path: /etc/config
    regexp: "^port"          # Matches "port=..."
    line: "listen_port=8080" # Doesn't start with "port"!
# Result: adds "listen_port=8080" every run

# RIGHT — regexp should match what you're adding
- lineinfile:
    path: /etc/config
    regexp: "^listen_port="
    line: "listen_port=8080"

Trailing Whitespace Issues

# Be specific with regexp to avoid whitespace mismatches
regexp: "^\\s*PermitRootLogin\\s+"

Conclusion

lineinfile is perfect for small, surgical file edits — one setting at a time. Use regexp + line to find-and-replace, state: absent to remove, and insertafter/insertbefore for precise placement. For whole-file management, use template. For multi-line blocks, use blockinfile. Always use validate for critical files (sudoers, sshd_config) and backup: true in production.