Introduction

The ansible.builtin.copy module copies files from the Ansible control node to remote hosts, or creates files with inline content. It handles permissions, ownership, backup, and is idempotent — it won't copy if the destination already matches.

Basic Syntax

# Copy a file
- name: Copy config file to remote
  ansible.builtin.copy:
    src: files/app.conf
    dest: /etc/myapp/app.conf

# Create a file from inline content
- name: Create file with content
  ansible.builtin.copy:
    content: "Hello, World!\n"
    dest: /tmp/hello.txt

Parameters

ParameterDefaultDescription
src—Local file path (mutually exclusive with content)
content—Inline content to write (mutually exclusive with src)
dest(required)Remote destination path
owner—File owner
group—File group
mode—File permissions (e.g., "0644")
backupfalseCreate backup of existing file
forcetrueOverwrite if different; false = copy only if missing
validate—Command to validate before final placement
remote_srcfalseCopy from remote to remote (not control → remote)
directory_mode—Mode for auto-created directories
followfalseFollow symlinks on the remote

Copy a File with Permissions

---
- name: Deploy configuration
  hosts: webservers
  become: true
  tasks:
    - name: Copy nginx config
      ansible.builtin.copy:
        src: files/nginx.conf
        dest: /etc/nginx/nginx.conf
        owner: root
        group: root
        mode: "0644"
        backup: true  # Keep backup of previous version
      notify: Reload nginx

  handlers:
    - name: Reload nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded

Create File with Inline Content

---
- name: Create files from content
  hosts: all
  become: true
  tasks:
    # Simple text file
    - name: Create MOTD
      ansible.builtin.copy:
        content: |
          =============================
          Welcome to {{ inventory_hostname }}
          Managed by Ansible
          =============================
        dest: /etc/motd
        mode: "0644"

    # JSON config
    - name: Create app config
      ansible.builtin.copy:
        content: |
          {
            "database": {
              "host": "{{ db_host }}",
              "port": 5432,
              "name": "{{ db_name }}"
            },
            "log_level": "{{ log_level | default('info') }}"
          }
        dest: /etc/myapp/config.json
        mode: "0640"
        owner: appuser

    # Script
    - name: Create health check script
      ansible.builtin.copy:
        content: |
          #!/bin/bash
          curl -sf http://localhost:8080/health || exit 1
        dest: /usr/local/bin/healthcheck.sh
        mode: "0755"

Copy vs Template

# copy — copies file AS-IS (no variable substitution in file content)
- ansible.builtin.copy:
    src: files/static-config.conf
    dest: /etc/myapp.conf

# template — processes Jinja2 variables and logic
- ansible.builtin.template:
    src: templates/dynamic-config.conf.j2
    dest: /etc/myapp.conf

# copy with content — supports Jinja2 in the content string
- ansible.builtin.copy:
    content: "server={{ inventory_hostname }}\n"
    dest: /etc/myapp.conf
Featurecopy (src)copy (content)template
Jinja2 in file❌✅✅
External file✅❌✅
Loops/conditionals❌Limited✅
Best forStatic filesSmall dynamic contentComplex templates

Copy Entire Directory

When src ends with /, it copies the contents. Without /, it copies the directory itself:

# Copy directory CONTENTS into /opt/app/
- name: Copy app files (contents only)
  ansible.builtin.copy:
    src: files/app/          # Trailing slash = contents
    dest: /opt/app/
    owner: deploy
    mode: "0644"
    directory_mode: "0755"
# Result: /opt/app/config.yml, /opt/app/start.sh, etc.

# Copy the directory ITSELF into /opt/
- name: Copy app directory
  ansible.builtin.copy:
    src: files/app           # No trailing slash = directory
    dest: /opt/
# Result: /opt/app/config.yml, /opt/app/start.sh, etc.

Copy from Remote to Remote

# Copy between paths on the SAME remote host
- name: Copy backup locally on remote
  ansible.builtin.copy:
    src: /etc/myapp/config.yml
    dest: /etc/myapp/config.yml.bak
    remote_src: true

# To copy FROM remote TO control node, use fetch module instead
- name: Fetch file from remote
  ansible.builtin.fetch:
    src: /var/log/myapp.log
    dest: logs/{{ inventory_hostname }}/
    flat: true

Validate Before Deploying

Run a validation command before the file is placed at its final destination:

# Validate sudoers syntax
- name: Deploy sudoers file
  ansible.builtin.copy:
    src: files/sudoers
    dest: /etc/sudoers
    validate: "visudo -cf %s"
    mode: "0440"

# Validate nginx config
- name: Deploy nginx config
  ansible.builtin.copy:
    src: files/nginx.conf
    dest: /etc/nginx/nginx.conf
    validate: "nginx -t -c %s"
    backup: true

%s is replaced with the temp file path. If validation fails, the original file is untouched.

Copy Only If Missing

- name: Copy default config (don't overwrite customized version)
  ansible.builtin.copy:
    src: files/default-config.yml
    dest: /etc/myapp/config.yml
    force: false  # Only copy if dest doesn't exist
    owner: appuser
    mode: "0644"

Copy Multiple Files

---
- name: Deploy multiple config files
  hosts: all
  become: true
  vars:
    config_files:
      - { src: "app.conf", dest: "/etc/myapp/app.conf", mode: "0644" }
      - { src: "db.conf", dest: "/etc/myapp/db.conf", mode: "0640" }
      - { src: "start.sh", dest: "/usr/local/bin/start-myapp.sh", mode: "0755" }

  tasks:
    - name: Copy all config files
      ansible.builtin.copy:
        src: "files/{{ item.src }}"
        dest: "{{ item.dest }}"
        owner: root
        group: root
        mode: "{{ item.mode }}"
        backup: true
      loop: "{{ config_files }}"

Practical Patterns

Deploy SSH Key

- name: Deploy authorized key
  ansible.builtin.copy:
    content: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}\n"
    dest: /home/deploy/.ssh/authorized_keys
    owner: deploy
    group: deploy
    mode: "0600"

Create Systemd Service File

- name: Deploy systemd service
  ansible.builtin.copy:
    content: |
      [Unit]
      Description=My Application
      After=network.target

      [Service]
      Type=simple
      User=appuser
      ExecStart=/opt/myapp/bin/start
      Restart=always
      RestartSec=5

      [Install]
      WantedBy=multi-user.target
    dest: /etc/systemd/system/myapp.service
    mode: "0644"
  notify:
    - Reload systemd
    - Restart myapp

Troubleshooting

"Could not find or access src"

The src path is relative to the role's files/ directory, or the playbook directory:

project/
├── playbook.yml
├── files/            ← copy looks here first
│   └── app.conf
└── roles/
    └── myrole/
        └── files/    ← copy looks here for role tasks
            └── app.conf

Large File Transfers Are Slow

For large files (>100MB), consider get_url or synchronize (rsync) instead:

# Faster for large files — uses rsync
- ansible.posix.synchronize:
    src: files/large-archive.tar.gz
    dest: /opt/

"Destination directory does not exist"

Create the directory first:

- ansible.builtin.file:
    path: /etc/myapp
    state: directory
    mode: "0755"

- ansible.builtin.copy:
    src: app.conf
    dest: /etc/myapp/app.conf

Conclusion

ansible.builtin.copy handles file deployment from control node to remote hosts. Use src for static files, content for inline dynamic content, and template for complex Jinja2 templates. Key features: validate prevents deploying broken configs, backup: true keeps the previous version, force: false skips if already present. For remote-to-local, use fetch. For large files, use synchronize (rsync).