Introduction

Copying files to Windows remote hosts is a fundamental task in Windows automation with Ansible. The ansible.windows.win_copy module provides a reliable way to transfer files from the Ansible controller to Windows targets over WinRM.

In this guide, you'll learn how to use win_copy effectively, including copying single files, directories, creating files from content, handling large files, and troubleshooting common issues.

The ansible.windows.win_copy Module

The full module name is ansible.windows.win_copy, part of the ansible.windows collection for managing Windows hosts. This module has been stable for years and is the standard way to copy files to Windows targets.

Key Characteristics

  • Transfers files over WinRM (not SSH)
  • Supports single files, directories, and inline content
  • Uses checksums to determine if transfer is needed (idempotent)
  • Not efficient for large files — use win_get_url instead for files hosted on web servers
  • Opposite operation: ansible.windows.win_fetch (remote → controller)
  • Linux equivalent: ansible.builtin.copy

Parameters Reference

ParameterTypeRequiredDefaultDescription
destpathYes—Remote absolute path (use \ for Windows paths)
srcpathNo*—Local source file/directory path
contentstringNo*—Text content to write directly to file
backupbooleanNofalseCreate backup before overwriting
forcebooleanNotrueTransfer even if file exists (set false to skip existing)
remote_srcbooleanNofalseIf true, src is on the remote host
decryptbooleanNotrueDecrypt Ansible Vault encrypted source files

*Either src or content is required, but not both.

Basic Examples

Copy a Single File

---
- name: Copy file to Windows host
  hosts: windows
  tasks:
    - name: Copy report to Desktop
      ansible.windows.win_copy:
        src: files/report.txt
        dest: C:\Users\admin\Desktop\report.txt

Copy with Backup

---
- name: Copy with backup
  hosts: windows
  tasks:
    - name: Update config with backup of original
      ansible.windows.win_copy:
        src: files/app.config
        dest: C:\Program Files\MyApp\app.config
        backup: true

Create File from Content

---
- name: Create file from content
  hosts: windows
  tasks:
    - name: Create environment config
      ansible.windows.win_copy:
        dest: C:\ProgramData\MyApp\settings.ini
        content: |
          [Settings]
          Environment=Production
          LogLevel=Warning
          MaxConnections=50
          ServerName={{ inventory_hostname }}

Advanced Patterns

Copy an Entire Directory

---
- name: Copy directory to Windows
  hosts: windows
  vars:
    app_dir: C:\Program Files\MyApp
  tasks:
    - name: Ensure app directory exists
      ansible.windows.win_file:
        path: "{{ app_dir }}"
        state: directory

    - name: Copy application files (directory contents)
      ansible.windows.win_copy:
        src: app_files/
        dest: "{{ app_dir }}\\"

Important: A trailing / on src copies only the contents. Without the trailing slash, the directory itself is copied as a subdirectory.

Copy from Remote Source

---
- name: Copy from network share
  hosts: windows
  tasks:
    - name: Copy installer from file share
      ansible.windows.win_copy:
        src: \\fileserver\share\installers\app-2.0.msi
        dest: C:\Temp\app-2.0.msi
        remote_src: true

Conditional Copy (Skip if Exists)

---
- name: Copy only if not present
  hosts: windows
  tasks:
    - name: Deploy initial config (don't overwrite customizations)
      ansible.windows.win_copy:
        src: files/default.config
        dest: C:\ProgramData\MyApp\user.config
        force: false

Copy with Variables and Loops

---
- name: Copy multiple files
  hosts: windows
  vars:
    config_files:
      - { src: 'web.config', dest: 'C:\inetpub\wwwroot\web.config' }
      - { src: 'app.config', dest: 'C:\Services\MyService\app.config' }
      - { src: 'nlog.config', dest: 'C:\Services\MyService\nlog.config' }
  tasks:
    - name: Copy configuration files
      ansible.windows.win_copy:
        src: "files/{{ item.src }}"
        dest: "{{ item.dest }}"
        backup: true
      loop: "{{ config_files }}"

Complete Playbook: Application Deployment

---
- name: Deploy Windows Application
  hosts: windows
  become: true
  become_method: runas
  become_user: SYSTEM
  vars:
    app_name: MyWebService
    app_dir: C:\Program Files\{{ app_name }}
    log_dir: C:\ProgramData\{{ app_name }}\Logs
    service_account: NT SERVICE\{{ app_name }}
  tasks:
    - name: Create application directories
      ansible.windows.win_file:
        path: "{{ item }}"
        state: directory
      loop:
        - "{{ app_dir }}"
        - "{{ log_dir }}"
        - "{{ app_dir }}\\config"

    - name: Copy application binaries
      ansible.windows.win_copy:
        src: "build/{{ app_name }}/"
        dest: "{{ app_dir }}\\"
      notify: Restart service

    - name: Deploy application configuration
      ansible.windows.win_copy:
        dest: "{{ app_dir }}\\config\\appsettings.json"
        content: |
          {
            "Logging": {
              "LogLevel": {
                "Default": "Information"
              },
              "FilePath": "{{ log_dir | regex_replace('\\\\', '\\\\\\\\') }}\\\\app.log"
            },
            "ConnectionStrings": {
              "Default": "Server={{ db_server }};Database={{ db_name }};Integrated Security=true"
            },
            "AppSettings": {
              "Environment": "{{ ansible_env.COMPUTERNAME }}",
              "MaxWorkers": 8
            }
          }
      notify: Restart service

    - name: Create Windows service health check script
      ansible.windows.win_copy:
        dest: "{{ app_dir }}\\healthcheck.ps1"
        content: |
          $response = Invoke-WebRequest -Uri "http://localhost:8080/health" -UseBasicParsing
          if ($response.StatusCode -eq 200) {
              Write-Output "Healthy"
              exit 0
          } else {
              Write-Output "Unhealthy: $($response.StatusCode)"
              exit 1
          }

  handlers:
    - name: Restart service
      ansible.windows.win_service:
        name: "{{ app_name }}"
        state: restarted

Performance Considerations

Since win_copy transfers data over WinRM, it's not efficient for large files. Consider these alternatives:

File SizeRecommended Approach
< 10 MBwin_copy is fine
10-100 MBwin_get_url from a web server or file share
> 100 MBwin_get_url or win_package with network source
Many small fileswin_copy with directory src
# Better for large files - download from web server
- name: Download large installer
  ansible.windows.win_get_url:
    url: https://artifacts.example.com/releases/app-2.0.msi
    dest: C:\Temp\app-2.0.msi
    checksum: sha256:abc123...

Troubleshooting

Common Error: Path Not Found

fatal: [win-host]: FAILED! => {"msg": "Destination directory C:\\NonExistent does not exist"}

Fix: Create the directory first with win_file:

- name: Ensure directory exists
  ansible.windows.win_file:
    path: C:\MyApp\Config
    state: directory

- name: Copy file
  ansible.windows.win_copy:
    src: config.xml
    dest: C:\MyApp\Config\config.xml

Common Error: Access Denied

fatal: [win-host]: FAILED! => {"msg": "Access is denied"}

Fix: Use become with appropriate privileges:

- name: Copy to protected directory
  ansible.windows.win_copy:
    src: files/service.config
    dest: C:\Windows\System32\config\service.config
  become: true
  become_method: runas
  become_user: Administrator

Common Error: WinRM Timeout on Large Files

Fix: Increase the WinRM timeout in your inventory or ansible.cfg:

[windows:vars]
ansible_winrm_operation_timeout_sec=120
ansible_winrm_read_timeout_sec=150

Conclusion

The ansible.windows.win_copy module is essential for Windows automation with Ansible. It handles single files, directories, and inline content creation with idempotent behavior via checksum verification. For large file transfers, prefer win_get_url or network shares. Always ensure target directories exist and your WinRM user has appropriate permissions.