Packer’s file provisioner can copy local files into your image during the build process, but it’s surprisingly tricky to get right because it operates within the context of the temporary build instance, not your final image.

Let’s see it in action. Imagine you have a my-app.conf file locally that you want to install into /etc/myapp/myapp.conf on your target image.

{
  "builders": [
    {
      "type": "amazon-ebs",
      "region": "us-east-1",
      "source_ami": "ami-0c55b159cbfafe1f0",
      "instance_type": "t2.micro",
      "ssh_username": "ec2-user",

      "ami_name": "my-app-image-{{timestamp}}"

    }
  ],
  "provisioners": [
    {
      "type": "file",
      "source": "my-app.conf",
      "destination": "/etc/myapp/myapp.conf"
    }
  ]
}

When Packer runs this, it starts an EC2 instance, connects to it, and then executes the provisioner. The file provisioner internally uses scp to copy my-app.conf from your local machine to the running EC2 instance’s /etc/myapp/myapp.conf. After the build completes, Packer stops the instance and creates an AMI from its disk.

The mental model here is that Packer is orchestrating a series of steps: launch an instance, run commands/copy files on that instance, then snapshot the instance’s disk. The file provisioner is just one of those steps, and crucially, it happens before the AMI is created. You are not directly modifying the source AMI; you are modifying a temporary instance that will become the new AMI.

The core problem this solves is automating the inclusion of configuration files, scripts, or any static assets into your machine images without needing to bake them into the source AMI or manually install them after launch. You define the desired state of your image’s filesystem in your Packer template.

The exact levers you control are the source and destination parameters. source can be a single file, a directory (which will be copied recursively), or a glob pattern relative to your Packer template file. destination is the absolute path on the temporary instance where the file or directory will be placed. If the destination directory doesn’t exist, Packer will attempt to create it.

A common pitfall is assuming the destination path is relative to the final image’s root filesystem in a way that’s independent of the temporary instance. It’s not. It’s a path on the temporary instance. If you try to copy to /tmp/my-file.txt and your source_ami is already running a system where /tmp is a separate mount, it works fine. But if you’re trying to copy to /etc/myapp/myapp.conf and /etc doesn’t exist on the temporary instance’s root filesystem (highly unlikely, but possible with custom AMIs or very minimal images), the copy would fail. Packer does attempt to create parent directories for the destination path, but this relies on the underlying OS tools and permissions of the temporary instance.

You can also use the file provisioner to upload entire directories. If your source is a directory named configs/ containing myapp.conf and another.conf, and your destination is /etc/myapp/, Packer will create /etc/myapp/ on the temporary instance and copy the contents of configs/ into it, resulting in /etc/myapp/myapp.conf and /etc/myapp/another.conf.

One thing most people don’t know is that the file provisioner honors scp’s behavior regarding file ownership and permissions. By default, scp attempts to preserve these from the source file on your local machine. This means if you have a file locally that’s owned by root:root with 0644 permissions, Packer will try to copy it to the temporary instance with those same ownership and permissions. This can be a blessing or a curse. If you need specific ownership or permissions, you might need to follow up with a shell provisioner to chown and chmod the files after they’ve been copied. For example, after the file provisioner, you might add:

{
  "type": "shell",
  "inline": [
    "chown root:root /etc/myapp/myapp.conf",
    "chmod 0644 /etc/myapp/myapp.conf"
  ]
}

This ensures the file has the correct ownership and permissions on the temporary instance before it’s snapshotted into your AMI.

The next thing you’ll likely run into is needing to upload files based on conditional logic or dynamically generated content, which the file provisioner alone doesn’t handle well.

Want structured learning?

Take the full Packer course →