chown changes the owner and the group of a file or a directory on Linux, and nothing else. The command writes the two fields that sit beside the permission string in a long listing, and the permission string belongs to a different program.
What sends readers searching is the reporting. The change line names the path you typed, and the next listing can still show the old owner, because the value is written into the inode and not every filesystem keeps one there.
What chown changes on a file
Every file and directory carries an inode, and the inode holds two numbers that name its owner and its group. chown writes those two numbers and leaves everything else on the inode alone.
The permission string is a separate field, so a file can change hands without a single read, write or execute bit moving. chmod owns that field, not this command.
A long listing prints both fields in fixed positions, which is why the listing is also the cheapest way to check a change.
| Column in ls -l | Field | What chown does with it |
|---|---|---|
| -rw-r–r– | File type and permission bits | Nothing at all. chmod writes this field |
| deploy | Owner, stored as a user id | Sets it when you name a user |
| webteam | Group, stored as a group id | Sets it when you name a group after the colon |
| 4096 | Size in bytes | Nothing |
| Sep 16 06:50 | Modification time | Nothing. touch updates it |
The tree below is what a deploy script leaves behind: a release folder, an upload folder the application writes to, and two symlinks. Every entry belongs to root:root, which is what you get when the script itself runs under sudo. I built it that way on purpose, because a folder the service only reads behaves differently from one it writes to.

Read the third and fourth fields on each line and the ownership column takes seconds to scan. The uploads directory shows drwxrwsr-x, and that s inside the group triplet is a setgid bit that new files inherit, not an ownership fact about the directory.
Only root can hand a file to another account. A regular user can move the group of a file they own, and cannot give ownership away at all, because the restriction sits in the kernel rather than in the file.
Membership in the target group is a separate job, and adding a user to a group in Linux covers it. That kernel restriction is also why every command below runs through sudo.
What you need before the first chown
Both names have to exist before chown can point at them, and each one is resolved against the local account database before the file is opened.
getent passwd deploy
getent group webteam
The account and the group are the two names I check first on a tree I did not create. An empty line from either command means the name is missing, and chown will refuse the operand rather than the file. Creating the account and creating the group are separate jobs, and users, groups and permissions covers both sides of that before you come back here.
The ownership operand itself has five shapes, and the shape decides which fields change.
| What you type | What changes |
|---|---|
| chown deploy file | The owner. The group keeps its current value |
| chown deploy:webteam file | The owner and the group |
| chown deploy: file | The owner, and the group resets to that account’s login group |
| chown :webteam file | The group only, which is what chgrp does |
| chown 1002:1003 file | Both fields, addressed by numeric id instead of by name |
Sudo is the other prerequisite, and the sudo command walks through granting it if your account cannot run it yet.
Change the owner of a file
Start with one file and one account, because the command reports exactly what it did to that pair and the report is what you have to learn to read.
The -v flag makes chown print a line per file instead of returning silently, which turns the command into its own receipt.
sudo chown -v deploy /var/www/app/index.html

The line names the path and both values, root before and deploy after. Only the owner is listed there, because only the owner changed.
Run the same command on a file that already belongs to deploy and the wording flips to retained, with an exit status of zero. A retained line answers the “I ran chown and nothing happened” question before you start looking at the filesystem, and it is the line I check for first.
| Line from chown -v | What it means |
|---|---|
| changed ownership of PATH from root to deploy | The owner was written. A group is named only when it changed too |
| ownership of PATH retained as deploy | The value already matched, so nothing was written |
| failed to change ownership of PATH | The change was refused. The edge cases section below has both causes |
A report that lists one value is not a truncated error, since chown names a field only when the value in it moved.
Change the group, or both fields at once
A group change looks like an ownership change with the owner left out, and the colon is the only thing that separates the two forms.
sudo chown -v :webteam /var/www/app/app.log

The report reads from root:root to :webteam, with the owner half blank on both sides of the change. That blank space is how chown says the owner was left alone.
chgrp does the same job under a different name. Run it against a file that already carries the group and it answers group of the path retained as webteam. That is the same retained wording chown uses.
Put a group on both sides of the colon and one command sets both fields. A colon with nothing after it does something different, because the owner is set and the group resets to that account’s login group.
sudo chown -v deploy:webteam /var/www/app/uploads
sudo chown -v 1002:1003 /var/www/app/releases/v2/index.html
Numeric ids address the same two fields, and the change line echoes the numbers you typed rather than the names behind them. They survive a rename of the account, which makes them the safer form inside a script that has to keep working after someone edits a username.
Change a whole tree with chown -R
A deployment directory holds hundreds of entries, and a recursive pass is how the application directory, the upload folder and every release inside it move together. The pass has two rules that need separating before you run it, one for directories and one for symlinks.
sudo chown -Rv deploy:webteam /var/www/app

The report above came back with 10 lines from a tree that holds 7 entries, because the pass also names the two release directories it walks through. Order is depth first, so the deepest file is reported before the folder that holds it and the directory you typed arrives last.
The word retained shows up on one of those lines, on the uploads folder that an earlier command had already moved. Retained lines keep a long recursive report readable, because the entries that stayed put show which part of the tree was already correct.
Symlinks follow a different rule, and that difference is what breaks ownership on a deployment tree. By default chown follows a link and rewrites the file it points at. The path in the report is then not the inode that changed.
sudo chown -v deploy /var/www/app/homepage

Add -h and the link becomes the object that changes. That flag is the only way to hand a link to another account without touching what it points at, and the listing afterwards is what proves the difference.
sudo chown -hv :webteam /var/www/app/homepage


That listing shows the split, with homepage at root:webteam and index.html at deploy:root. The link and its target are separate inodes with separate owners, and creating symbolic links in Ubuntu covers what a link is when you have not met one yet.
Inside a recursive pass the rule changes again, because -R rewrites each symlink it meets as a link and never steps into the directory behind it. I set up a shared folder outside the tree to check that, and the link entry changed while the file inside it kept its owner.
That default is what keeps a shared folder outside the application directory safe from a deploy script, and the traversal options decide when to override it.
| Option | What -R does with a symlink to a directory |
|---|---|
| -P, the default | Rewrites the link entry and never enters the directory it points at |
| -H | Follows a symlink you name on the command line, and leaves the ones it finds inside the tree alone |
| -L | Follows every symlink to a directory it meets anywhere in the pass |
The diagram below draws the same rule as one pass, ending on the step where a shared folder outside the application directory survives a deploy script untouched.

Adding -L changes the outcome on that last step, because the pass enters the symlinked directory and rewrites its contents. The file inside the shared folder then ends up owned by the application account, which is the right result when that folder belongs to the deployment and the wrong one when a different team mounts it.
Copy an owner from another file, or change only what matches
A pair of flags turns chown from a blunt instrument into a conditional one. Both matter on a directory that should not be rewritten wholesale.
The first copies both fields from a file you already trust, with no ownership operand at all and only a reference.
sudo chown -v --reference=/var/www/app/index.html /var/www/app/app.log
The change line reports app.log moving from deploy:webteam to deploy:root, which is the pair index.html carried at that moment. A reference file is always followed when it happens to be a symlink, so the values come from the file it points at.
The second flag skips anything that does not already match, which is the guard you want after a restore has scattered root-owned files through a tree.
sudo chown -Rv --from=root:root deploy:webteam /var/www/app
That run reported 10 lines and exactly 1 change. The log file that had come back from the archive as root:root moved to deploy:webteam, and every entry that already matched was reported as retained, which is the behaviour I want from a guard.
A plain rewrite would have touched the whole tree, including the symlinks, so the guard earns its extra words inside a cron job. When the copy itself is what loses the owner, preserving permissions and ownership while copying covers the tar and rsync flags that keep it intact.
| Flag | Job | Reach for it when |
|---|---|---|
| –reference | Copy both fields from another file | A known-good file already carries the pair you want, and typing the names invites a typo |
| –from | Act only on files that match the current owner | A restore or a rebuild left part of the tree owned by root and you do not want to rewrite the rest |
When chown refuses to change anything
Nearly every report of chown doing nothing lands in one of four states, and each one needs a different fix.
chown deploy /var/www/app/index.html

The kernel refuses an ownership change from an unprivileged process, whatever the file’s own permissions say. Being the owner of the file is not enough, because handing a file to somebody else is a privilege change rather than a permission on the file. Run the same command through sudo and it works.
The identical message appears when you are already root and the filesystem is the problem. A FAT stick keeps no owner per file at all, because the mount options set one owner for everything on the volume.
sudo mount -o uid=1002,gid=1003 /dev/sdb1 /mnt/drive
Mount that volume without uid and gid and the listing reports root:root for every file, taken from the process that mounted it, and chown answers Operation not permitted even for root. I tested both mounts on the same image, and the only difference between them was the option list: with uid and gid set the listing shows the account I asked for, because the mount is now supplying the owner rather than the disk itself.
The man page states the rule for the fat driver in one line, with uid and gid setting the owner and group of all files and defaulting to the user and group of the process that mounted the volume.
A name that does not exist produces a different failure, and it stops the command before the file is opened.
sudo chown bogus /var/www/app/index.html

Check the account with getent passwd and the group with getent group before you try the command again. An account that was deleted from the system leaves its numeric id behind on the files. The listing then shows the number where a name belongs, which is why an old backup can look like it has a broken owner.
| Message | Cause | Check |
|---|---|---|
| chown: changing ownership of PATH: Operation not permitted | No root privileges, or a filesystem that stores no owner per file | Run it under sudo, then look at the mount options |
| chown: invalid user: NAME | The account does not exist on this machine | getent passwd NAME |
| chown: invalid group: NAME | The group does not exist, or the colon is in the wrong place | getent group NAME |
| ownership of PATH retained as NAME | Nothing needed to change | Read the listing again before changing anything else |
Verify the tree, then hand it to the service
A change is only finished when a second command agrees with the first. The check itself takes two commands, and one of them answers the question a deploy script actually asks.
stat -c '%U %G %n' /var/www/app/index.html
find /var/www/app -not -user deploy -not -type l
The stat line prints the account and group as names against the path, which compresses the listing down to the two fields you care about, and it is the output I read first on a machine I have not touched before. The find sweep is the one to keep in a script, because it prints the entries that are still owned by another account and prints nothing at all when the tree is clean.
Excluding symlinks from the sweep is deliberate. A link carries its own owner, and the service reads the file it points at. A root-owned link above a correctly owned target is therefore not a problem worth failing a deploy over.
- Run chown -R as the last step of the deploy instead of after the first error
- Keep the group on the upload folder and let the setgid bit carry it to new files
- Add the find sweep to the same script and fail the deploy when it prints anything
The listing is the receipt, and chown reports the files it changed and nothing about the ones it skipped, the symlink it followed on your behalf, or the mounted volume that never had an owner to write to. My habit is to run the sweep as the last line of the deploy and treat any output from it as a failure, which is what turns a command that looked successful into a change the service can actually use.
FAQ
What does the chown command do in Linux?
It sets the owner and the group of a file or a directory, using the two id numbers stored in the inode. It does not change the read, write and execute bits, which belong to chmod.
How do I change the owner of a directory in Linux?
Run sudo chown -R deploy:webteam /path/to/directory. The -R makes the pass cover every file and subdirectory underneath, and the account you name has to exist before the command runs.
What is the difference between chown and chmod?
chown writes the owner and group fields, and chmod writes the permission bits. The long listing shows both side by side, which is why a file can change hands while its permissions stay exactly as they were.
Why does chown say Operation not permitted when I own the file?
Handing a file to another account is a privilege change, so the kernel allows it only for root. A second cause is a filesystem that stores no owner per file, such as FAT, where chown fails even under sudo because there is no field to write.
How do I change the owner and the group at the same time?
Name both after one colon, as in chown deploy:webteam file. A colon with nothing after it sets the owner and resets the group to that account’s login group.
Does chown work on a USB drive formatted with FAT or exFAT?
No. Those filesystems keep no owner per file, so the owner in the listing comes from the uid and gid mount options. Set them at mount time with -o uid=1002,gid=1003 instead of running chown afterwards.
