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
| Parameter | Default | Description |
|---|---|---|
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") |
backup | false | Create backup of existing file |
force | true | Overwrite if different; false = copy only if missing |
validate | — | Command to validate before final placement |
remote_src | false | Copy from remote to remote (not control → remote) |
directory_mode | — | Mode for auto-created directories |
follow | false | Follow 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
| Feature | copy (src) | copy (content) | template |
|---|---|---|---|
| Jinja2 in file | ❌ | ✅ | ✅ |
| External file | ✅ | ❌ | ✅ |
| Loops/conditionals | ❌ | Limited | ✅ |
| Best for | Static files | Small dynamic content | Complex 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
Related Articles
- Ansible template Module
- Ansible fetch Module (Remote → Local)
- Ansible file Module
- Ansible lineinfile Module
- Ansible win_copy (Windows)
- Ansible Create Directory Guide
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).