Introduction
Creating text files on remote servers is one of the most common tasks in infrastructure automation. Whether you need configuration files, scripts, status markers, or simple text content, the Ansible copy module provides an elegant solution using the content parameter.
In this guide, you'll learn how to create text files with Ansible using ansible.builtin.copy, including single-line content, multi-line files, dynamic content with variables, proper permissions, and SELinux contexts.
The ansible.builtin.copy Module
The full module name is ansible.builtin.copy, part of the built-in collection shipped with Ansible. While its primary purpose is copying files from the controller to remote hosts, the content parameter enables direct file creation without a source file.
When to Use copy vs template
| Use Case | Module |
|---|---|
| Static text content | ansible.builtin.copy with content |
| Content with variables/logic | ansible.builtin.template |
| Copy existing file | ansible.builtin.copy with src |
| Complex Jinja2 formatting | ansible.builtin.template |
| Simple variable substitution | Either works |
Rule of thumb: If your content has {% %} blocks, loops, or complex conditionals, use template. For static or simple variable content, copy is simpler and more readable.
Parameters Reference
| Parameter | Type | Required | Description |
|---|---|---|---|
dest | path | Yes | Remote absolute path for the file |
content | string | No* | Text content to write to the file |
src | path | No* | Source file on the controller |
mode | string | No | File permissions (e.g., '0644') |
owner | string | No | File owner |
group | string | No | File group |
backup | boolean | No | Create backup before overwriting |
force | boolean | No | Replace file even if it exists (default: yes) |
setype | string | No | SELinux type context |
seuser | string | No | SELinux user context |
selevel | string | No | SELinux level context |
validate | string | No | Validation command before final placement |
*Either content or src is required, but not both.
Basic Examples
Create a Simple Text File
---
- name: Create text files
hosts: all
tasks:
- name: Create a simple text file
ansible.builtin.copy:
dest: /tmp/hello.txt
content: "Hello, World!\n"
mode: '0644'
Create a Multi-Line File
Use YAML block scalars (|) for multi-line content:
---
- name: Create multi-line files
hosts: all
tasks:
- name: Create a configuration file
ansible.builtin.copy:
dest: /etc/myapp/config.txt
content: |
# Application Configuration
app_name=myservice
app_port=8080
log_level=info
max_connections=100
owner: root
group: root
mode: '0644'
Create an Empty File
---
- name: Create empty file
hosts: all
tasks:
- name: Create an empty marker file
ansible.builtin.copy:
dest: /var/lib/myapp/.initialized
content: ""
mode: '0644'
Advanced Patterns
Using Variables in Content
---
- name: Create file with variables
hosts: all
vars:
app_name: "myservice"
app_version: "2.1.0"
deploy_timestamp: "{{ ansible_date_time.iso8601 }}"
tasks:
- name: Create version info file
ansible.builtin.copy:
dest: /opt/{{ app_name }}/VERSION
content: |
Application: {{ app_name }}
Version: {{ app_version }}
Deployed: {{ deploy_timestamp }}
Host: {{ inventory_hostname }}
mode: '0644'
Create File with Backup
---
- name: Create file with backup
hosts: all
tasks:
- name: Update motd with backup of previous version
ansible.builtin.copy:
dest: /etc/motd
content: |
==========================================
Welcome to {{ inventory_hostname }}
Managed by Ansible - Do not edit manually
==========================================
backup: true
mode: '0644'
Conditional File Creation
---
- name: Conditional file creation
hosts: all
tasks:
- name: Create environment-specific config
ansible.builtin.copy:
dest: /etc/myapp/environment
content: "ENVIRONMENT={{ 'production' if inventory_hostname in groups['prod'] else 'staging' }}\n"
mode: '0644'
- name: Create file only if directory exists
ansible.builtin.copy:
dest: /opt/myapp/status.txt
content: "running\n"
mode: '0644'
when: myapp_installed | default(false)
Set SELinux Context
---
- name: Create file with SELinux context
hosts: all
tasks:
- name: Create web content file
ansible.builtin.copy:
dest: /var/www/html/status.html
content: |
<html><body><h1>Service OK</h1></body></html>
owner: apache
group: apache
mode: '0644'
setype: httpd_sys_content_t
Validate Before Writing
---
- name: Create and validate configuration
hosts: all
tasks:
- name: Create sudoers entry (validated)
ansible.builtin.copy:
dest: /etc/sudoers.d/deploy
content: |
deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart myapp
mode: '0440'
validate: /usr/sbin/visudo -csf %s
Complete Playbook Example
Here's a comprehensive playbook demonstrating multiple file creation patterns:
---
- name: Server initialization - Create required files
hosts: all
become: true
vars:
app_user: "appservice"
app_dir: "/opt/myapp"
log_dir: "/var/log/myapp"
tasks:
- name: Create application directory
ansible.builtin.file:
path: "{{ app_dir }}"
state: directory
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: '0755'
- name: Create application config file
ansible.builtin.copy:
dest: "{{ app_dir }}/app.conf"
content: |
[server]
bind_address = 0.0.0.0
port = 8080
workers = 4
[logging]
level = info
path = {{ log_dir }}/app.log
max_size = 100M
rotate = 7
[database]
host = localhost
port = 5432
name = myapp_db
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: '0640'
notify: Restart application
- name: Create systemd environment file
ansible.builtin.copy:
dest: /etc/default/myapp
content: |
APP_HOME={{ app_dir }}
APP_USER={{ app_user }}
APP_ENV=production
JAVA_OPTS="-Xmx512m -Xms256m"
mode: '0644'
notify: Restart application
- name: Create logrotate configuration
ansible.builtin.copy:
dest: /etc/logrotate.d/myapp
content: |
{{ log_dir }}/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 {{ app_user }} {{ app_user }}
}
mode: '0644'
handlers:
- name: Restart application
ansible.builtin.systemd:
name: myapp
state: restarted
Common Mistakes and Solutions
Mistake 1: Missing Newline at End of File
# Bad - no trailing newline
- name: Create file
ansible.builtin.copy:
dest: /tmp/test.txt
content: "last line"
# Good - explicit newline
- name: Create file
ansible.builtin.copy:
dest: /tmp/test.txt
content: "last line\n"
# Good - block scalar adds newline automatically
- name: Create file
ansible.builtin.copy:
dest: /tmp/test.txt
content: |
last line
Mistake 2: Using content with Variables Unsafely
# Bad - can produce unpredictable output with special chars
- name: Create file
ansible.builtin.copy:
dest: /tmp/test.txt
content: "{{ user_input }}"
# Better - use template module for complex variable content
- name: Create file
ansible.builtin.template:
src: test.txt.j2
dest: /tmp/test.txt
Mistake 3: Not Setting Permissions
# Bad - default permissions may be insecure
- name: Create secrets file
ansible.builtin.copy:
dest: /etc/myapp/secrets.conf
content: "db_password=secret123\n"
# Good - explicit restrictive permissions
- name: Create secrets file
ansible.builtin.copy:
dest: /etc/myapp/secrets.conf
content: "db_password={{ vault_db_password }}\n"
owner: root
group: myapp
mode: '0640'
Idempotency
The copy module is fully idempotent. Ansible checks the content checksum before making changes:
- If the file exists with identical content → no change (green/ok)
- If the file doesn't exist or content differs → changed (yellow)
- The
backupparameter preserves the previous version before overwriting
This makes it safe to run repeatedly without unintended side effects.
Related Modules
- ansible.builtin.template — For files with Jinja2 logic
- ansible.builtin.file — For creating directories and managing file attributes
- ansible.builtin.lineinfile — For modifying specific lines in existing files
- ansible.builtin.blockinfile — For managing blocks of text within files
Conclusion
The ansible.builtin.copy module with the content parameter is the simplest way to create text files on remote hosts. It's idempotent, supports permissions and SELinux contexts, and handles both simple and multi-line content elegantly. For more complex templating needs with Jinja2 logic, use ansible.builtin.template instead.