Ansible copy vs template vs lineinfile: When to Use Each Module
By Luca Berton · Published 2024-01-01 · Category: troubleshooting
Comparison of Ansible copy, template, and lineinfile modules. When to use each for file management, templates, single-line edits.
Three Ansible modules manage file content: copy, template, and lineinfile. Each solves a different problem. Using the wrong one creates brittle playbooks. This guide explains when to use each, with real examples and a decision flowchart.
Quick Comparison
| Feature | copy | template | lineinfile |
|---------|------|----------|------------|
| Use case | Static files | Dynamic files | Edit single lines |
| Jinja2 support | content: only | Full Jinja2 | line:/regexp: only |
| Source | Local file or inline | Jinja2 .j2 file | Existing remote file |
| Creates file | Yes | Yes | Configurable |
| Idempotent | Yes | Yes | Yes |
| Replaces entire file | Yes | Yes | No |
| Preserves unknown content | No | No | Yes |
| Speed | Fastest | Fast | Moderate |
See also: ansible.builtin.file Module: Manage Files, Directories & Symlinks (Complete Guide)
The Decision Flowchart
Do you control the ENTIRE file content?
├── YES → Is the content dynamic (variables, loops, conditions)?
│ ├── YES → Use TEMPLATE
│ └── NO → Use COPY
└── NO → Do you need to change specific lines in an existing file?
├── YES → Does the file have a predictable format?
│ ├── YES → Use LINEINFILE (or blockinfile for multi-line)
│ └── NO → Use TEMPLATE with careful defaults
└── NO → Use COPY for a new file, TEMPLATE if dynamic
Module 1: copy
Use copy when you have a static file or inline content with no variables.
Copy a Local File
- name: Deploy static configuration
ansible.builtin.copy:
src: files/motd.txt
dest: /etc/motd
owner: root
group: root
mode: '0644'
Inline Content
- name: Create static config
ansible.builtin.copy:
content: |
# Application Configuration
LOG_LEVEL=info
MAX_CONNECTIONS=100
TIMEOUT=30
dest: /etc/myapp/config.env
mode: '0644'
When to Use copy
• Static configuration files (no variables) • Binary files (images, certificates, archives) • Simple text files that don't change between hosts • Scripts that don't need host-specific valuesWhen NOT to Use copy
# ❌ WRONG — hardcoded values that differ per host
- name: Deploy host config
ansible.builtin.copy:
content: |
HOSTNAME=webserver01
IP=192.168.1.10
dest: /etc/myapp/host.conf
# ✅ RIGHT — use template instead
- name: Deploy host config
ansible.builtin.template:
src: host.conf.j2
dest: /etc/myapp/host.conf
See also: Ansible Write to File: 5 Methods with Practical Examples (2026)
Module 2: template
Use template when file content needs variables, conditionals, or loops.
Basic Template
# playbook
- name: Deploy dynamic config
ansible.builtin.template:
src: templates/nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
mode: '0644'
notify: reload nginx
{# templates/nginx.conf.j2 #}
worker_processes {{ ansible_processor_vcpus }};
events {
worker_connections {{ nginx_worker_connections | default(1024) }};
}
http {
server {
listen {{ nginx_port | default(80) }};
server_name {{ inventory_hostname }};
{% for upstream in nginx_upstreams | default([]) %}
location {{ upstream.path }} {
proxy_pass http://{{ upstream.host }}:{{ upstream.port }};
}
{% endfor %}
}
}
Template with Conditionals
{# templates/sshd_config.j2 #}
Port {{ ssh_port | default(22) }}
PermitRootLogin {{ 'yes' if allow_root_login | default(false) else 'no' }}
PasswordAuthentication {{ 'yes' if allow_password_auth | default(false) else 'no' }}
{% if ssh_allowed_users is defined %}
AllowUsers {{ ssh_allowed_users | join(' ') }}
{% endif %}
{% if ssh_banner_enabled | default(true) %}
Banner /etc/ssh/banner.txt
{% endif %}
When to Use template
• Configuration files with host-specific values • Files that change based on group membership • Files with conditional sections • Files with repeated blocks (loops) • Any file where you want Ansible variables resolvedTemplate vs copy with content
# copy with content — works for simple cases
- ansible.builtin.copy:
content: "HOSTNAME={{ inventory_hostname }}\n"
dest: /etc/hostname.conf
# template — better for anything complex
- ansible.builtin.template:
src: hostname.conf.j2
dest: /etc/hostname.conf
# Rule of thumb: if you have more than 2 variables
# or any conditional, use template
Module 3: lineinfile
Use lineinfile when you need to change ONE line in an existing file without touching the rest.
Ensure a Line Exists
- name: Enable IP forwarding in sysctl
ansible.builtin.lineinfile:
path: /etc/sysctl.conf
regexp: '^net\.ipv4\.ip_forward'
line: 'net.ipv4.ip_forward = 1'
state: present
Modify Existing Line
- name: Change SSH port
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?Port\s+'
line: 'Port 2222'
notify: restart sshd
Add Line After Match
- name: Add nameserver after existing ones
ansible.builtin.lineinfile:
path: /etc/resolv.conf
insertafter: '^nameserver'
line: 'nameserver 8.8.8.8'
Remove a Line
- name: Remove obsolete config
ansible.builtin.lineinfile:
path: /etc/myapp.conf
regexp: '^DEPRECATED_OPTION='
state: absent
When to Use lineinfile
• Modifying system files you don't fully control (/etc/sysctl.conf, /etc/fstab)
• Toggling a single setting in a large config
• Adding entries to files managed by other tools
• Files where you only care about one specific line
When NOT to Use lineinfile
# ❌ WRONG — managing many lines with lineinfile
- name: Configure app
ansible.builtin.lineinfile:
path: /etc/myapp.conf
regexp: "^{{ item.key }}="
line: "{{ item.key }}={{ item.value }}"
loop:
- { key: DB_HOST, value: "db.example.com" }
- { key: DB_PORT, value: "5432" }
- { key: DB_NAME, value: "myapp" }
- { key: DB_USER, value: "appuser" }
- { key: LOG_LEVEL, value: "info" }
# ^^^ If managing 5+ lines, use template instead
# ✅ RIGHT — use template for the whole file
- name: Deploy app config
ansible.builtin.template:
src: myapp.conf.j2
dest: /etc/myapp.conf
See also: Automating File Extension Validation with Ansible
blockinfile: The Middle Ground
When you need to manage multiple lines but can't own the whole file:
- name: Add custom rules to iptables
ansible.builtin.blockinfile:
path: /etc/sysconfig/iptables
marker: "# {mark} ANSIBLE MANAGED - custom rules"
block: |
-A INPUT -p tcp --dport 8080 -j ACCEPT
-A INPUT -p tcp --dport 8443 -j ACCEPT
-A INPUT -p tcp --dport 9090 -j ACCEPT
insertbefore: "^COMMIT"
Performance Considerations
# Fastest → slowest for deploying a full config file:
# 1. copy (src: file) — raw file transfer, no processing
# 2. copy (content: ...) — inline, minimal processing
# 3. template — Jinja2 rendering + transfer
# 4. lineinfile (×N) — reads file N times for N lines
# For 100 hosts deploying a config:
# copy: ~2s
# template: ~3s
# lineinfile: ~5s (single line), ~15s (5 lines in loop)
Real-World Examples
Example 1: /etc/hosts
# Use lineinfile — you need to add entries without removing existing ones
- name: Add application hosts
ansible.builtin.lineinfile:
path: /etc/hosts
regexp: '.*{{ item.hostname }}$'
line: "{{ item.ip }} {{ item.hostname }}"
loop:
- { ip: "10.0.1.10", hostname: "db.internal" }
- { ip: "10.0.1.20", hostname: "cache.internal" }
Example 2: Application Config File
# Use template — you control the whole file
- name: Deploy application config
ansible.builtin.template:
src: app.conf.j2
dest: /etc/myapp/app.conf
validate: "/opt/myapp/bin/validate-config %s"
Example 3: SSL Certificate
# Use copy — binary/static file
- name: Deploy SSL certificate
ansible.builtin.copy:
src: "certs/{{ inventory_hostname }}.pem"
dest: /etc/ssl/certs/app.pem
mode: '0644'
FAQ
When should I use copy content instead of template?
Use copy content: for simple inline strings with 1-2 variables and no conditionals. Use template when you have complex logic, loops, conditionals, or more than a few variables. The threshold is roughly 5 lines of content or 2+ variables.
Can lineinfile create a file?
Yes, by default. If the file doesn't exist and create: true (which is the default when state: present), lineinfile creates it. Set create: false to fail if the file is missing.
Is lineinfile idempotent?
Yes — if the line already matches, no change is made. The regexp parameter finds the line to replace, and line sets the desired content. Running it twice produces the same result.
Should I use lineinfile or template for /etc/sysctl.conf?
For 1-3 settings, lineinfile is fine. For comprehensive sysctl hardening (10+ settings), consider the ansible.posix.sysctl module or a template that owns the entire file (e.g., a drop-in file in /etc/sysctl.d/).
What about the replace module?
ansible.builtin.replace uses regex to find and replace text anywhere in a file, not just complete lines. Use it when you need to change part of a line (e.g., update a version number inside a URL).
Conclusion
copy for static files. template for dynamic files. lineinfile for surgical single-line edits. When in doubt, use template — it's the most flexible and keeps your configuration fully version-controlled.
Related Articles
• Ansible Write to File: Complete Guide • Ansible Jinja2 Filters Complete Reference • Ansible Variable Precedence Guide • Ansible register: Save Task OutputCategory: troubleshooting