Ansible Error Guide: 'Destination directory does not exist' copy/template Failure
Fix Ansible's copy/template 'Destination directory ... does not exist' error: diagnose missing parent paths, wrong dest, file vs dir confusion, and permission issues.
- #ansible
- #troubleshooting
- #errors
- #files
Exact Error Message
fatal: [app-01]: FAILED! => {
"changed": false,
"checksum": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3",
"msg": "Destination directory /etc/myapp/conf.d does not exist"
}
The same class of error from the template and copy modules can also read Destination /opt/app/releases/current not writable or dst path /etc/myapp/app.conf does not exist, but the most common form is the missing parent directory shown above.
What the Error Means
The copy, template, and unarchive modules write a file to a dest: path. They will create the destination file, but they do not, by default, create the parent directories leading up to it. If the directory that should contain the file does not exist on the remote host, the module fails with Destination directory ... does not exist.
In other words, Ansible found a valid source and a valid file name but had nowhere to put it because the containing folder is absent.
Common Causes
- The parent directory simply was not created before the copy/template task ran (no preceding
file: state=directorytask). - A role assumed a package created the directory (e.g.
/etc/nginx/conf.d) but the package is not installed yet. dest:ends without a trailing slash where a directory was intended, or has a typo in the path.- Task ordering: the directory-creating task runs later in the play, or in a role that has not been applied.
- The directory exists but the
becomeuser cannot traverse into it (surfacing as not writable rather than not existing). - A templated
dest:resolved to an unexpected path because a variable was empty or wrong.
How to Reproduce the Error
Copy a file into a directory that has not been created:
- hosts: app
become: true
tasks:
- name: Deploy app config
ansible.builtin.template:
src: app.conf.j2
dest: /etc/myapp/conf.d/app.conf
owner: root
mode: "0644"
ansible-playbook deploy.yml -i inventory.ini --check --diff -vvv
fatal: [app-01]: FAILED! => {"changed": false, "msg": "Destination directory /etc/myapp/conf.d does not exist"}
Diagnostic Commands
Run the play in check mode with verbosity to see the exact resolved destination:
ansible-playbook deploy.yml -i inventory.ini --check --diff -vvv
Confirm what the destination path actually resolves to (in case it is templated):
ansible app-01 -i inventory.ini -m debug -a "msg={{ '/etc/myapp/conf.d/app.conf' }}"
Check whether the parent directory exists and who owns it on the host:
ansible app-01 -i inventory.ini -b -m command -a "ls -ld /etc/myapp /etc/myapp/conf.d"
Verify the path with the stat module to distinguish “missing” from “not a directory”:
ansible app-01 -i inventory.ini -b -m stat -a "path=/etc/myapp/conf.d"
Step-by-Step Resolution
-
Read the resolved destination from the error. Confirm it is the path you actually intended and not the result of an empty variable.
-
Create the parent directory first. Add an explicit
filetask before the copy/template:
- name: Ensure config directory exists
ansible.builtin.file:
path: /etc/myapp/conf.d
state: directory
owner: root
mode: "0755"
- name: Deploy app config
ansible.builtin.template:
src: app.conf.j2
dest: /etc/myapp/conf.d/app.conf
mode: "0644"
-
Fix task ordering. If another role creates the directory, make this role depend on it or move the directory task earlier.
-
Check for trailing-slash and typo mistakes.
dest: /etc/myapp/conf.d/(with a slash) treats the target as a directory and copies the source file into it; without it the last segment is the file name. Use the form that matches your intent. -
If the directory exists but is “not writable,” fix ownership/permissions or ensure
become: trueruns as a user that can write there. -
Re-run in check mode, then apply:
ansible-playbook deploy.yml -i inventory.ini --check --diff
changed: [app-01]
Prevention and Best Practices
- Always pair a file write with an explicit
file: state=directorytask that ensures the parent path exists; do not rely on a package or earlier role to have made it. - Keep directory-creation tasks at the top of a role’s task file so every later file write has a guaranteed home.
- Guard templated
dest:paths with| default(...)so an empty variable cannot collapse the path to something unintended. - Be deliberate about trailing slashes in
dest:to avoid file-vs-directory confusion. - Run roles in check mode in CI; a missing directory shows up immediately without changing hosts.
- Use
modeandownerconsistently so a directory that exists but is unwritable does not turn into a confusing “not writable” failure later.
Related Errors
Destination ... not writable— the directory exists but the become user lacks permission.dst path ... not foundfromunarchive— the extraction target directory is missing.Could not find or access '...' on the Ansible Controller— a missing source file, not a missing destination.failed to transfer file to ...— an SSH/SFTP transport problem rather than a missing directory.
Frequently Asked Questions
Why does Ansible not just create the directory for me? The copy/template modules create the destination file but intentionally do not create arbitrary parent directories, to avoid silently making paths in the wrong place. Create parents explicitly with the file module.
My dest path looks wrong in the error. It is probably templated from a variable that was empty or mistyped. Print it with debug and guard it with | default(...).
Trailing slash or not? With a trailing slash, dest: is treated as a directory and the source file is placed inside it. Without it, the final path segment is the file name. Pick the one matching your intent.
It says the directory does not exist but I can see it. Run stat on the path: it may be a file, a broken symlink, or unreadable by the become user, which Ansible reports differently. For more file and templating patterns, see the Ansible guides.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.