Ansible Copy File from Local to Remote — Complete Guide
Introduction
The ansible.builtin.copy module transfers files from the Ansible controller (local machine) to remote hosts. It handles permissions, ownership, backups, validation, and content generation — all idempotently. This guide covers every copy pattern you'll need.
Quick Reference
# Copy single file
- ansible.builtin.copy:
src: files/app.conf
dest: /etc/myapp/app.conf
# Copy with permissions
- ansible.builtin.copy:
src: scripts/deploy.sh
dest: /usr/local/bin/deploy.sh
owner: root
group: root
mode: '0755'
# Generate file from string content
- ansible.builtin.copy:
content: "Hello World\n"
dest: /tmp/hello.txt
# Copy directory recursively
- ansible.builtin.copy:
src: configs/
dest: /etc/myapp/
Parameters
| Parameter | Default | Description |
|---|---|---|
src | — | Local file/directory path |
dest | (required) | Remote destination path |
content | — | String content (instead of src) |
owner | — | File owner on remote |
group | — | File group on remote |
mode | — | File permissions (e.g., '0644') |
backup | false | Create backup before overwrite |
force | true | Overwrite if different |
validate | — | Command to validate before placing |
remote_src | false | If true, src is on the remote host |
directory_mode | — | Permissions for created directories |
follow | false | Follow symlinks |
Copy Patterns
Single File
- name: Deploy configuration
ansible.builtin.copy:
src: nginx.conf
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
notify: Restart nginx
File with Backup
- name: Update config with backup
ansible.builtin.copy:
src: app.conf
dest: /etc/myapp/app.conf
backup: true # Creates /etc/myapp/app.conf.2026-06-01@12:00:00~
Generate Content Inline
- name: Create environment file
ansible.builtin.copy:
content: |
DATABASE_URL=postgresql://{{ db_host }}:5432/{{ db_name }}
REDIS_URL=redis://{{ redis_host }}:6379
SECRET_KEY={{ secret_key }}
dest: /etc/myapp/.env
owner: app
group: app
mode: '0600'
Recursive Directory Copy
# Copy entire directory (trailing slash = contents only)
- name: Deploy static assets
ansible.builtin.copy:
src: static/ # Trailing slash: copies contents INTO dest
dest: /var/www/html/
# Without trailing slash: copies the directory itself
- name: Deploy app directory
ansible.builtin.copy:
src: myapp # No slash: creates /opt/myapp on remote
dest: /opt/
Validate Before Placing
- name: Deploy sudoers file (validate syntax first)
ansible.builtin.copy:
src: sudoers
dest: /etc/sudoers
validate: "visudo -cf %s" # %s = temp file path
- name: Deploy nginx config (validate first)
ansible.builtin.copy:
src: nginx.conf
dest: /etc/nginx/nginx.conf
validate: "nginx -t -c %s"
Copy from Remote to Remote
- name: Copy file within remote host
ansible.builtin.copy:
src: /etc/myapp/config.bak
dest: /etc/myapp/config.conf
remote_src: true
copy vs template vs synchronize
| Module | Use When |
|---|---|
copy | Static files, no Jinja2 needed |
template | Files with variables (Jinja2 templates) |
synchronize | Large directories, rsync-based (fast) |
fetch | Remote → local (reverse direction) |
# Use template when content has variables
- ansible.builtin.template:
src: app.conf.j2
dest: /etc/myapp/app.conf
# Use synchronize for large directories (faster than copy)
- ansible.posix.synchronize:
src: /local/large-dir/
dest: /remote/large-dir/
Performance: Large File Transfers
# For many files or large directories, synchronize is faster:
- name: Fast recursive copy with rsync
ansible.posix.synchronize:
src: "{{ playbook_dir }}/files/website/"
dest: /var/www/html/
delete: true # Remove files on remote not in source
rsync_opts:
- "--compress"
- "--exclude=.git"
Common Patterns
Deploy Multiple Config Files
- name: Deploy configuration files
ansible.builtin.copy:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
owner: "{{ item.owner | default('root') }}"
mode: "{{ item.mode | default('0644') }}"
loop:
- { src: "app.conf", dest: "/etc/myapp/app.conf" }
- { src: "db.conf", dest: "/etc/myapp/db.conf", mode: "0600" }
- { src: "logrotate.conf", dest: "/etc/logrotate.d/myapp" }
notify: Restart application
Copy Only If Missing
- name: Deploy default config (don't overwrite existing)
ansible.builtin.copy:
src: default.conf
dest: /etc/myapp/config.conf
force: false # Only copies if dest doesn't exist
Copy with SELinux Context
- name: Deploy with SELinux context
ansible.builtin.copy:
src: index.html
dest: /var/www/html/index.html
setype: httpd_sys_content_t
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
Permission denied | Can't write to dest | Use become: true or fix dest permissions |
Source file not found | Wrong path | Path is relative to files/ dir in role, or playbook dir |
could not find src | File doesn't exist locally | Check the file exists at the specified path |
Destination not writable | Directory doesn't exist | Create directory first with file module |
| Task always shows "changed" | File content differs (line endings?) | Check for CRLF vs LF, ensure content matches |
File Search Path
Ansible looks for src files in this order:
files/directory adjacent to the playbook- Current playbook directory
files/directory in the role (if in a role)
project/
├── playbook.yml
├── files/
│ └── app.conf ← Found with src: app.conf
└── roles/
└── webapp/
└── files/
└── app.conf ← Found in role context
Best Practices
- Use
templatefor dynamic content — don't usecopy+contentwith complex Jinja2 - Always set
mode— explicit is better than inheriting umask - Use
validatefor critical configs — prevents deploying broken sudoers/nginx - Set
backup: truefor important files — easy rollback - Use
synchronizefor large dirs — significantly faster than recursive copy - Use
force: falsefor defaults — don't overwrite user-modified configs
Conclusion
Use ansible.builtin.copy for static file transfers from local to remote. Set owner, group, and mode explicitly. Use validate for syntax-checked configs. Use content for small generated files. For templates with variables, use template instead. For large directories, use synchronize for rsync-based speed.