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

ParameterDefaultDescription
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')
backupfalseCreate backup before overwrite
forcetrueOverwrite if different
validate—Command to validate before placing
remote_srcfalseIf true, src is on the remote host
directory_mode—Permissions for created directories
followfalseFollow 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

ModuleUse When
copyStatic files, no Jinja2 needed
templateFiles with variables (Jinja2 templates)
synchronizeLarge directories, rsync-based (fast)
fetchRemote → 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

ErrorCauseFix
Permission deniedCan't write to destUse become: true or fix dest permissions
Source file not foundWrong pathPath is relative to files/ dir in role, or playbook dir
could not find srcFile doesn't exist locallyCheck the file exists at the specified path
Destination not writableDirectory doesn't existCreate 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:

  1. files/ directory adjacent to the playbook
  2. Current playbook directory
  3. 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

  1. Use template for dynamic content — don't use copy + content with complex Jinja2
  2. Always set mode — explicit is better than inheriting umask
  3. Use validate for critical configs — prevents deploying broken sudoers/nginx
  4. Set backup: true for important files — easy rollback
  5. Use synchronize for large dirs — significantly faster than recursive copy
  6. Use force: false for 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.