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 CaseModule
Static text contentansible.builtin.copy with content
Content with variables/logicansible.builtin.template
Copy existing fileansible.builtin.copy with src
Complex Jinja2 formattingansible.builtin.template
Simple variable substitutionEither 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

ParameterTypeRequiredDescription
destpathYesRemote absolute path for the file
contentstringNo*Text content to write to the file
srcpathNo*Source file on the controller
modestringNoFile permissions (e.g., '0644')
ownerstringNoFile owner
groupstringNoFile group
backupbooleanNoCreate backup before overwriting
forcebooleanNoReplace file even if it exists (default: yes)
setypestringNoSELinux type context
seuserstringNoSELinux user context
selevelstringNoSELinux level context
validatestringNoValidation 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 backup parameter preserves the previous version before overwriting

This makes it safe to run repeatedly without unintended side effects.

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.