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.
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 contentsThe 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
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 # AlpineThey 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
| Task | Command |
|---|---|
| Zip a directory, recursively | zip -r out.zip dir/ |
| Zip a few files | zip out.zip a.txt b.txt |
| Zip the contents, not the folder | cd dir && zip -r ../out.zip . |
| Leave files out | zip -r out.zip dir/ -x '*.log' '*/cache/*' |
| Quiet, no file list | zip -rq out.zip dir/ |
| Best compression | zip -r -9 out.zip dir/ |
| Add new and changed files only | zip -ru out.zip dir/ |
| Mirror the folder (also drop deleted files) | zip -r -FS out.zip dir/ |
| Delete one entry from an archive | zip -d out.zip 'dir/.env' |
| Store files without their folders | zip -j out.zip dir/a.txt dir/b.txt |
| Zip a list of files from find | find dir -name '*.css' | zip out.zip -@ |
| Split into 100 MB parts | zip -r -s 100m out.zip dir/ |
| List an archive | unzip -l out.zip |
| Test an archive for damage | unzip -t out.zip |
| Extract into a directory | unzip out.zip -d /srv/restore |
How to zip a directory in Linux: zip -r
$ 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:
$ 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 fileNo 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
cd site && zip -r ../site.zip . # entries are index.html, css/..., not site/index.htmlUse ., 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:
| Pattern | Result |
|---|---|
-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 |
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:
| Level | Archive 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:
-uadds 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 underset -etreats as a failure.-FS(file sync) also removes entries whose files are gone. In the test it printeddeleting: site/debug.logafter that log had been removed from disk. Run it from the directory you created the archive in, so the paths match.-ddeletes 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
$ 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.ziplists entries with sizes and dates, and the last line gives the total uncompressed size, which tells you how much disk the extraction needs.unzip -treads every entry and checks its CRC without writing anything. Run it on a backup before you rely on it.unzip archive.zip -d /srv/restoreextracts into a directory. It creates the last directory but not missing parents:-d deep/new/dirfailed withcheckdir: cannot create extraction directoryuntilmkdir -pcreated 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
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 -llisted every name and size of the protected archive without asking for a password. - A wrong password gives
skipping: site/index.html incorrect passwordand 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:
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 unpackSplitting a large zip into parts: -s
$ 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:
zip -s 0 split.zip --out whole.zip
unzip -t whole.zipzip -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.zipzip 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.
| zip | tar.gz | |
|---|---|---|
| Many small, similar files | Per-file headers add up; can end larger than the data | Much smaller: the whole stream is compressed together |
| List or extract one file | Fast: reads the table at the end | Has to decompress from the start |
| Unix owner and permissions | Mode is restored; owner only with unzip -X | Owner, group and mode restored when extracting as root |
| Symbolic links | Followed unless you add -y | Stored as links |
| Opens on Windows without extra software | Yes, in File Explorer | Yes, with the built-in tar.exe on Windows 10 (1803 and later) and 11 |
| Encryption | Weak ZipCrypto only | None built in; pipe through gpg |
| Typical use | Sending files to people, Windows users, panels | Server 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:
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 sizeThen 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.zipandExpand-Archive. Microsoft’s documentation gives a 2 GB maximum file size forCompress-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.