Guides

zip command in Linux: zip a directory, exclude files, split and encrypt

Linux zip command: zip -r a directory, exclude with -x, update, split and password options, unzip -l/-t/-d, and zip vs tar.gz with measured sizes.

Published ~8 min read
On this page

Short answer: zip -r archive.zip folder/ packs a folder and everything in it into one ZIP file; without -r, zip stores only the empty folder. Add -x 'pattern' to leave files out and -q to hide the file list. unzip -l archive.zip shows what is inside, and unzip archive.zip -d target/ extracts it.

any Linux
sudo apt install -y zip unzip                     # Debian, Ubuntu
zip -r site.zip site/                             # zip a directory
zip -r site.zip site/ -x '*/node_modules/*' '*.log'   # leave things out
unzip -l site.zip                                 # list the contents

The commands and outputs below were run on Debian 12 with Zip 3.0 and UnZip 6.00 (Debian packages 3.0-13+deb12u1 and 6.0-28+deb12u1), the Info-ZIP versions that current Linux distributions ship. The 7-Zip and gpg commands and the zip -e prompt follow their documentation and were not run.

Install zip and unzip

install both
sudo apt update && sudo apt install -y zip unzip   # Debian, Ubuntu
sudo dnf install -y zip unzip                      # Rocky, AlmaLinux, RHEL, Fedora
apk add zip unzip                                  # Alpine

They are two separate small packages, and minimal server and container images often include neither. command -v zip unzip prints a path for each one that is installed. If the shell answers unzip: command not found or apt cannot find the package, unzip: command not found covers every distribution and the Docker case.

zip and unzip cheat sheet

TaskCommand
Zip a directory, recursivelyzip -r out.zip dir/
Zip a few fileszip out.zip a.txt b.txt
Zip the contents, not the foldercd dir && zip -r ../out.zip .
Leave files outzip -r out.zip dir/ -x '*.log' '*/cache/*'
Quiet, no file listzip -rq out.zip dir/
Best compressionzip -r -9 out.zip dir/
Add new and changed files onlyzip -ru out.zip dir/
Mirror the folder (also drop deleted files)zip -r -FS out.zip dir/
Delete one entry from an archivezip -d out.zip 'dir/.env'
Store files without their folderszip -j out.zip dir/a.txt dir/b.txt
Zip a list of files from findfind dir -name '*.css' | zip out.zip -@
Split into 100 MB partszip -r -s 100m out.zip dir/
List an archiveunzip -l out.zip
Test an archive for damageunzip -t out.zip
Extract into a directoryunzip out.zip -d /srv/restore

How to zip a directory in Linux: zip -r

bash
$ zip -r site.zip site
  adding: site/ (stored 0%)
  adding: site/index.html (deflated 93%)
  adding: site/css/ (stored 0%)
  adding: site/css/style.css (deflated 94%)
  adding: site/cache/ (stored 0%)
  adding: site/cache/page.html (stored 0%)
  adding: site/.env (stored 0%)

The archive name comes first, then what to put in it. deflated 93% means the file shrank by 93%; stored 0% is normal for directories and for files too small to gain from compression. The trap is leaving out -r:

bash
$ zip nor.zip site
  adding: site/ (stored 0%)
$ unzip -l nor.zip
Archive:  nor.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
        0  2026-10-21 19:43   site/
---------                     -------
        0                     1 file

No error, exit status 0, and an archive with one empty folder in it. Always check a new archive with unzip -l before you delete anything or copy it off the server.

Zip the contents without the top folder

bash
cd site && zip -r ../site.zip .    # entries are index.html, css/..., not site/index.html

Use ., not *. The shell expands * before zip runs, and a plain * does not match names that start with a dot: in the test above, zip -r ../g.zip * left .env out of the archive without a word, while zip -r ../g.zip . included it. The same goes for .htaccess and any other dotfile.

Excluding files and folders: -x

zip matches -x patterns against the path as it is stored in the archive, including the top folder, and its * also matches across /. Quote every pattern so the shell passes it to zip unexpanded. With node_modules folders at two levels, three patterns behaved like this:

PatternResult
-x 'node_modules/*'Nothing excluded: no stored path starts with node_modules/
-x 'site/node_modules/*'Only the top-level folder excluded; site/sub/node_modules stayed
-x '*/node_modules/*'Both excluded, including the folder entries themselves
a typical website backup
zip -rq site-backup.zip site \
  -x '*/node_modules/*' '*/.git/*' '*/cache/*' '*.log'

zip reads every argument after -x as a pattern until the next option, so put -x after the archive name and the folder, at the end of the command. The opposite, -i, keeps only matching files: zip -r css.zip site -i '*.css'. Keep secrets out on purpose (-x '*/.env') if the archive leaves the server, or remove one afterwards with zip -d site.zip 'site/.env'.

Quiet output, compression levels and update modes

-q suppresses the adding: lines, which is what you want in cron jobs and scripts. Compression goes from -0 (store only) to -9; the default is -6. On 17 plain-text licence files (303,076 bytes, from /usr/share/common-licenses on Debian 12) the levels gave:

LevelArchive size
-0 (store)305,754 bytes
-1 (fastest)124,753 bytes
-6 (default)109,015 bytes
-9 (best)108,907 bytes

Going from the default to -9 saved 108 bytes here, so the default is usually right. Already compressed files (JPEG, PNG, MP4, other archives) do not shrink; -n .jpg:.png:.mp4 stores them without trying, which saves CPU time on big media folders.

Updating an existing archive: -u, -FS, -d

Running zip -r on an archive that already exists does not start over: it adds new files, replaces changed ones and keeps entries for files you have since deleted. Three options make that deliberate:

  • -u adds new files and replaces changed ones. With nothing to do it prints nothing and exits with status 12 (“zip has nothing to do” in the man page), which a script running under set -e treats as a failure.
  • -FS (file sync) also removes entries whose files are gone. In the test it printed deleting: site/debug.log after that log had been removed from disk. Run it from the directory you created the archive in, so the paths match.
  • -d deletes named entries: zip -d site.zip 'site/.env'.

Two more that come up: -j stores files without their directory path (handy for mailing three log files), and -y stores a symbolic link as a link. Without -y, zip follows the link and stores a copy of the file it points to.

Listing, testing and extracting with unzip

bash
$ unzip -t site.zip
Archive:  site.zip
    testing: site/                    OK
    testing: site/index.html          OK
    testing: site/css/                OK
    testing: site/css/style.css       OK
    testing: site/cache/              OK
    testing: site/cache/page.html     OK
    testing: site/.env                OK
No errors detected in compressed data of site.zip.
  • unzip -l archive.zip lists entries with sizes and dates, and the last line gives the total uncompressed size, which tells you how much disk the extraction needs.
  • unzip -t reads every entry and checks its CRC without writing anything. Run it on a backup before you rely on it.
  • unzip archive.zip -d /srv/restore extracts into a directory. It creates the last directory but not missing parents: -d deep/new/dir failed with checkdir: cannot create extraction directory until mkdir -p created the parents.

Overwrite prompts (-o, -n), garbled file names from Windows archives and the “cannot find zipfile directory” error are covered in the unzip command section of our unzip guide.

Password-protected zip: -e, and why not to rely on it

bash
zip -er private.zip private/      # prompts for the password twice

-e asks for the password at a prompt. Avoid -P password: the password then sits in your shell history and is visible to other users in ps while zip runs. Both use the old PKZip 2.0 scheme (often called ZipCrypto), which zip -h2 itself describes as “standard (weak) PKZip 2.0 encryption”. Two more limits showed up in the test:

  • File names are not encrypted: unzip -l listed every name and size of the protected archive without asking for a password.
  • A wrong password gives skipping: site/index.html incorrect password and exit status 82, so a script can tell it apart from a damaged archive.

For database dumps, keys or customer data, use real encryption. 7-Zip’s own format uses AES-256, and -mhe=on hides the file names too. On Debian and Ubuntu the package is p7zip-full or, on newer releases, 7zip; afterwards command -v 7z 7zz shows which command name you got. Or pipe a tarball through GnuPG:

stronger options
7z a -p -mhe=on secrets.7z secrets/                  # prompts for the password

tar -czf - secrets/ | gpg -c --cipher-algo AES256 -o secrets.tar.gz.gpg
gpg -d secrets.tar.gz.gpg | tar -xzf -               # decrypt and unpack

Splitting a large zip into parts: -s

bash
$ zip -q -s 1m split.zip blob.bin
$ wc -c split.z*
1048576 split.z01
1048576 split.z02
 403403 split.zip
2500555 total

-s takes a size with k, m, g or t. The last part always carries the .zip name. UnZip 6.00 cannot read a split set directly: it warns that “true multi-part support doesn’t exist yet” and failed with bad zipfile offset. The documented way back is to rejoin the parts first:

any Linux
zip -s 0 split.zip --out whole.zip
unzip -t whole.zip
split a finished archive instead
zip -rq backup.zip /var/www/site
split -b 1G -d backup.zip backup.zip.part-       # backup.zip.part-00, -01, ...
cat backup.zip.part-* > backup.zip               # on the other side
unzip -t backup.zip

zip vs tar.gz on Linux

ZIP compresses each file on its own and keeps a table of contents at the end; a tar.gz is one stream compressed as a whole. That difference decides most of the table, and it shows clearly on many small files: 2,000 small JSON files (205,786 bytes in total) became a 498,829 byte ZIP, larger than the input, and a 40,878 byte tar.gz.

ziptar.gz
Many small, similar filesPer-file headers add up; can end larger than the dataMuch smaller: the whole stream is compressed together
List or extract one fileFast: reads the table at the endHas to decompress from the start
Unix owner and permissionsMode is restored; owner only with unzip -XOwner, group and mode restored when extracting as root
Symbolic linksFollowed unless you add -yStored as links
Opens on Windows without extra softwareYes, in File ExplorerYes, with the built-in tar.exe on Windows 10 (1803 and later) and 11
EncryptionWeak ZipCrypto onlyNone built in; pipe through gpg
Typical useSending files to people, Windows users, panelsServer backups and moving sites between Linux servers

For a Linux-to-Linux move or a backup you restore on Linux, use tar.gz; the tar.gz guide has the create and extract commands. Use ZIP when the archive goes to someone on Windows or into a web panel’s upload form.

Zipping on a server

A dated backup of a site before you change it, without the junk:

bash
zip -rq ~/site-$(date +%F).zip /var/www/example.com \
  -x '*/cache/*' '*/node_modules/*' '*.log'
unzip -l ~/site-$(date +%F).zip | tail -1        # entry count and total size

Then copy it to your own machine with the scp command, or see transferring files to a VPS for rsync and graphical clients. The archive counts against the server’s disk until you delete it, so remove old ones once they are copied off.

  • On a Windows VPS: PowerShell has Compress-Archive -Path C:\site\* -DestinationPath C:\backup\site.zip and Expand-Archive. Microsoft’s documentation gives a 2 GB maximum file size for Compress-Archive, so use 7-Zip above that.
  • On Plesk web hosting: the File Manager unpacks an uploaded ZIP with Extract Files, so you do not need a shell for a one-off upload.

Frequently asked

How do I zip a directory in Linux?

Run zip -r archive.zip directory/. The -r option makes zip recurse into the folder and all its subfolders; without it, zip stores only the empty directory entry and still exits without an error. Check the result with unzip -l archive.zip.

How do I exclude a folder when zipping?

Add -x with a quoted pattern at the end, for example zip -r site.zip site -x '*/node_modules/*'. zip matches the pattern against the path stored in the archive, including the top folder, so 'node_modules/*' on its own matches nothing when you zip the parent folder.

Is a password-protected zip secure?

Not with zip -e or -P. Both use the old PKZip 2.0 encryption, which zip's own help calls weak, and the file names stay readable without the password. For sensitive files, use 7-Zip's AES-256 format with 7z a -p -mhe=on, or encrypt a tarball with gpg -c.

Should I use zip or tar.gz on Linux?

Use tar.gz for backups and for moving files between Linux servers: it keeps owners and symbolic links and compresses many small files far better. Use zip for archives that go to Windows users or into a web panel, since Windows opens ZIP files without extra software.

Related articles

All guides