Skip to main content

zip and unzip - Work with ZIP archives

ZIP is a widely supported archive format that works well across Linux, macOS, and Windows. On a WordPress VPS, it is most useful for transfers and handoffs (client delivery, moving plugin/theme bundles, shipping a site snapshot) where portability matters.

Quick Summary
  • Create a ZIP: zip -r site.zip /var/www/html
  • Exclude patterns: zip -r site.zip /var/www/html -x '*.log' '*/cache/*'
  • List contents: unzip -l site.zip
  • Test integrity: unzip -t site.zip
  • Extract to a directory: unzip site.zip -d /tmp/restore-test

When to use ZIP

  • Cross-platform sharing (Windows/macOS users).
  • Packaging plugins/themes.
  • Quick file snapshots for migration.

If you care about Linux ownership/permissions and want faster restores, prefer tar-based backups (tar.zst, tar.gz).


Prerequisites

  • Access Level: Normal user (sudo if working in /var/www/).
  • Packages: Install if missing.
install-zip-unzip.sh
sudo apt update
sudo apt install -y zip unzip

zip-prerequisites-002.sh
which zip

zip-prerequisites-004.sh
zip -v

zip-prerequisites-006.sh
zip --version

zip-prerequisites-008.sh
dpkg -l | grep zip

  • Knowledge: Comfortable with file paths, cd, and permissions.

Notes on paths and portability

  • Output path can be a filename or an absolute path (for example /backups/site.zip).
  • Input targets can be one or many files/directories.
  • ZIP restores are portable, but metadata fidelity is not as strong as tar on Linux.

Syntax breakdown

zip Syntax

zip-zip-syntax-010.sh
zip [OPTIONS] TARGET_LOCATION/OUTPUT_FILE.zip INPUT_FILES_OR_DIRECTORIES

SegmentDescriptionExampleNotes
zipThe command to create or update a zip archive.zipMust be installed (sudo apt install zip).
[OPTIONS]Flags that modify behavior (e.g., recursive, compression level, exclude).-r, -9, -xCombine as needed.
OUTPUT_FILE.zipDestination path and filename of the archive./root/backup.zipShould end with .zip. Can include full or relative path.
INPUT_FILES_OR_DIRECTORIESOne or more files or directories to include./var/www/html, index.php, config/Can be absolute or relative paths.

unzip Syntax

zip-unzip-syntax-012.sh
unzip [OPTIONS] ARCHIVE.zip [-d TARGET_DIRECTORY]


SegmentDescriptionExampleNotes
unzipCommand used to extract .zip archives.unzipMust be installed (sudo apt install unzip).
[OPTIONS]Modifiers to control extraction behavior.-o, -l, -t, -dControl overwrite, listing, testing, etc.
ARCHIVE.zipThe name (and path) of the ZIP file to extract./root/html_backup.zipCan use absolute or relative path.
-d TARGET_DIRECTORYDirectory where files will be extracted.-d /var/www/html_restoreOptional — default is current directory.

Options and flags

OptionMeaningExampleNotes
-rRecursively include subdirectories.zip -r site.zip /var/www/htmlEssential for folders.
-9Use maximum compression.zip -9 backup.zip file.txtSlower but smaller file.
-qQuiet mode (no output).zip -rq backup.zip htmlUseful for cron jobs.
-xExclude files or patterns.zip -r backup.zip html -x "*.log" "*.cache/*"Handy for skipping caches/logs.
-uUpdate changed files in an existing zip.zip -ru backup.zip htmlKeeps archive fresh without recreating.
-fFreshen (update existing entries only).zip -rf backup.zip htmlDoesn’t add new files.
-dDelete entries from an existing archive.zip -d backup.zip *.tmpRemoves unwanted files.
-mMove files into zip (delete originals).zip -rm backup.zip old_logs/Use with caution!
-jJunk paths — don’t store directory structure.zip -rj backup.zip html/*Flattens files (dangerous for duplicates).
-eEncrypt with password.zip -re backup.zip htmlPrompts for password.
-TTest archive integrity after creation.zip -rT backup.zip htmlGood for verification.

Core syntax

1. Create a ZIP archive of a directory

zip-create-directory-001.sh
zip -r [ARCHIVE_NAME].zip [SOURCE_DIRECTORY]

# Example:
# zip -r /home/wpbackup/site.zip /var/www/html
# Input directory: /var/www/html
# Output file: /home/wpbackup/site.zip

2. Create a ZIP archive of a single file

zip-create-single-file-002.sh
zip [ARCHIVE_NAME].zip [FILE_PATH]

# Example:
# zip /home/wpbackup/config.zip /var/www/html/wp-config.php
# Input file: /var/www/html/wp-config.php
# Output file: /home/wpbackup/config.zip

3. Create a ZIP with maximum compression

zip-create-max-compression-003.sh
zip -r -9 [ARCHIVE_NAME].zip [SOURCE_DIRECTORY]

# Example:
# zip -r -9 /home/wpbackup/site-max.zip /var/www/html
# Input directory: /var/www/html
# Output file: /home/wpbackup/site-max.zip
# Levels: -1 (fastest) to -9 (best compression)

4. Create a ZIP excluding cache and logs

zip-create-exclude-patterns-004.sh
zip -r [ARCHIVE_NAME].zip [SOURCE_DIRECTORY] -x '[PATTERN1]' '[PATTERN2]'

# Example:
# zip -r /home/wpbackup/site.zip /var/www/html \
# -x '*/cache/*' \
# -x '*.log' \
# -x '*/uploads/cache/*'
# Input directory: /var/www/html
# Output file: /home/wpbackup/site.zip (without cache dirs and logs)

5. Create an encrypted ZIP (password prompt)

zip-create-encrypted-005.sh
zip -r -e [ARCHIVE_NAME].zip [SOURCE_DIRECTORY]

# Example:
# zip -r -e /home/wpbackup/site-secure.zip /var/www/html
# Input directory: /var/www/html
# Output file: /home/wpbackup/site-secure.zip
# Note: -e prompts for password interactively (do NOT use -P password inline)

6. Create a split ZIP (multi-part)

zip-create-split-archive-006.sh
zip -r -s [SIZE] [ARCHIVE_NAME].zip [SOURCE_DIRECTORY]

# Example:
# zip -r -s 100m /home/wpbackup/site-split.zip /var/www/html
# Input directory: /var/www/html
# Output parts: /home/wpbackup/site-split.z01, .z02 ... site-split.zip
# Size units: k (KB), m (MB), g (GB)

7. Create a timestamped ZIP (cron-friendly)

zip-create-timestamped-007.sh
zip -r "/[BACKUP_DIR]/[ARCHIVE_NAME]_$(date +%F).zip" [SOURCE_DIRECTORY]

# Example:
# zip -r "/home/wpbackup/site_$(date +%F).zip" /var/www/html
# Input directory: /var/www/html
# Output file: /home/wpbackup/site_2025-10-07.zip

8. List contents of a ZIP archive

zip-list-contents-008.sh
unzip -l [ARCHIVE_NAME].zip

# Example:
# unzip -l /home/wpbackup/site.zip
# Input file: /home/wpbackup/site.zip
# Output: file list with sizes and dates printed to terminal

9. Test integrity of a ZIP archive

zip-test-integrity-009.sh
unzip -t [ARCHIVE_NAME].zip

# Example:
# unzip -t /home/wpbackup/site.zip
# Input file: /home/wpbackup/site.zip
# Output: "No errors detected in compressed data." on success

10. Extract a ZIP to a staging directory

zip-extract-to-staging-010.sh
unzip [ARCHIVE_NAME].zip -d [OUTPUT_DIRECTORY]

# Example:
# unzip /home/wpbackup/site.zip -d /tmp/restore-test
# Input file: /home/wpbackup/site.zip
# Output directory: /tmp/restore-test

11. Extract a single file from a ZIP

zip-extract-single-file-011.sh
unzip [ARCHIVE_NAME].zip [STORED_FILE_PATH] -d [OUTPUT_DIRECTORY]

# Example:
# unzip /home/wpbackup/site.zip var/www/html/wp-config.php -d /tmp/restore-test
# Input file: /home/wpbackup/site.zip
# Output: /tmp/restore-test/var/www/html/wp-config.php

12. Append a file to an existing ZIP archive

zip-append-file-012.sh
zip [ARCHIVE_NAME].zip [FILE_TO_ADD]

# Example:
# zip /home/wpbackup/site.zip /var/www/html/robots.txt
# Input archive: /home/wpbackup/site.zip
# Appended file: /var/www/html/robots.txt

13. Delete a file from inside a ZIP archive

zip-delete-from-archive-013.sh
zip -d [ARCHIVE_NAME].zip [STORED_FILE_PATH]

# Example:
# zip -d /home/wpbackup/site.zip var/www/html/readme.html
# Input archive: /home/wpbackup/site.zip
# Deleted entry: var/www/html/readme.html

14. Batch-extract multiple ZIP archives

zip-batch-extract-014.sh
for z in [DIRECTORY]/*.zip; do unzip -o "$z" -d [OUTPUT_DIRECTORY]; done

# Example:
# for z in /home/wpbackup/*.zip; do unzip -o "$z" -d /tmp/extracted/; done
# Input: all .zip files in /home/wpbackup/
# Output directory: /tmp/extracted/

Common commands (examples)

Create a zip of a single file

zip-create-a-zip-of-a-single-file-014.sh
zip file.zip index.php

zip-create-a-zip-of-a-single-file-016.sh
adding: index.php (deflated 20%)

Use Case: Quickly package a config file.

Benefit: Lightweight transfer.


Create a zip of a directory recursively

zip-create-a-zip-of-a-directory-recursively-018.sh
zip -r site.zip /var/www/html/

zip-create-a-zip-of-a-directory-recursively-020.sh
adding: html/ (stored 0%)
adding: html/wp-config.php (deflated 42%)

Use Case: Backup full WordPress site.

Benefit: Single portable file.


Maximum compression

zip-maximum-compression-022.sh
zip -r -9 site_max.zip /var/www/html

zip-maximum-compression-024.sh
adding: html/wp-content/plugins/ (deflated 75%)

Use Case: Reduce size for slow transfers.

Benefit: Saves bandwidth.


Quiet mode (no logs)

zip-quiet-mode-no-logs-026.sh
zip -rq quiet.zip *

(no output)

Use Case: Clean automation scripts.

Benefit: No cluttered logs.


Encrypt a zip

zip-encrypt-a-zip-028.sh
zip -re secure.zip wp-content/

zip-encrypt-a-zip-030.sh
Enter password:
Verify password:
adding: wp-content/...

Use Case: Protect sensitive files.

Benefit: Safer sharing.


Exclude a directory

zip-exclude-a-directory-032.sh
zip -r site_no_cache.zip /var/www/html -x "*/cache/*"

zip-exclude-a-directory-034.sh
skipping: html/wp-content/cache/

Use Case: Ignore cache.

Benefit: Smaller, cleaner archive.


List contents of a zip

zip-list-contents-of-a-zip-036.sh
unzip -l site.zip

zip-list-contents-of-a-zip-038.sh
Length Method Size Name
------ ------ ---- ----
23456 Defl:N 6789 wp-config.php

Use Case: Verify before extracting.

Benefit: Avoids surprises.


Extract to current directory

zip-extract-to-current-directory-040.sh
unzip site.zip

zip-extract-to-current-directory-042.sh
extracting: wp-config.php

Use Case: Quick restore.

Benefit: Default simple use.


Extract to a custom directory

zip-extract-to-a-custom-directory-044.sh
unzip site.zip -d /tmp/restore/

zip-extract-to-a-custom-directory-046.sh
extracting: /tmp/restore/wp-content/index.php

Use Case: Testing restore.

Benefit: Keeps clean environment.


Overwrite existing files

zip-overwrite-existing-files-048.sh
unzip -o site.zip

zip-overwrite-existing-files-050.sh
replace index.php? [y]es, [n]o, [A]ll:
inflating: index.php

Use Case: Auto updates.

Benefit: Saves manual confirmation.


Extract while ignoring paths

zip-extract-while-ignoring-paths-052.sh
unzip -j plugins.zip

zip-extract-while-ignoring-paths-054.sh
extracting: plugin1.php

Use Case: Only files, no dirs.

Benefit: Clean flat output.


Test ZIP integrity

zip-test-zip-integrity-056.sh
unzip -t site.zip

zip-test-zip-integrity-058.sh
No errors detected in compressed data.

Use Case: Validate backups.

Benefit: Prevents corrupt restores.


Create multiple ZIPs from a file list

zip-create-multiple-zips-from-a-file-list-060.sh
zip files.zip $(cat filelist.txt)

zip-create-multiple-zips-from-a-file-list-062.sh
adding: index.html
adding: style.css

Use Case: Controlled packaging.

Benefit: Customizable.


Add files to an existing archive

zip-add-files-to-an-existing-archive-064.sh
zip update.zip newfile.txt

zip-add-files-to-an-existing-archive-066.sh
updating: newfile.txt (stored 0%)

Use Case: Append logs.

Benefit: No need to rebuild.


Extract only a specific file

zip-extract-only-a-specific-file-068.sh
unzip site.zip wp-config.php

zip-extract-only-a-specific-file-070.sh
extracting: wp-config.php

Use Case: Recover only config.

Benefit: Saves time.


Delete a file inside a ZIP

zip-delete-a-file-inside-a-zip-072.sh
zip -d site.zip readme.html

zip-delete-a-file-inside-a-zip-074.sh
deleting: readme.html

Use Case: Remove junk.

Benefit: Cleaner archive.


Create a split ZIP (multi-part)

zip-create-a-split-zip-multi-part-076.sh
zip -r -s 100m bigsite.zip /var/www/html

zip-create-a-split-zip-multi-part-078.sh
creating: bigsite.z01
creating: bigsite.z02

Use Case: Upload in parts.

Benefit: Bypass file size limits.


Extract multiple ZIPs in a batch

zip-extract-multiple-zips-in-a-batch-080.sh
for z in *.zip; do unzip -o "$z" -d extracted/; done

Use Case: Bulk plugin installs.

Benefit: Automation.


Zip the current folder only

zip-zip-the-current-folder-only-082.sh
zip -r current.zip .

zip-zip-the-current-folder-only-084.sh
adding: ./index.php

Use Case: Snapshot folder.

Benefit: Quick backup.


Combine zip with move

zip-combine-zip-with-move-086.sh
zip -r site.zip /var/www/html && mv site.zip /backups/

zip-combine-zip-with-move-088.sh
adding: html/...

Use Case: Package + move.

Benefit: Automates backup path.


Benefits

  • Portable, widely supported.
  • Faster uploads compared to raw folders.
  • Supports encryption.
  • Compatible with WordPress plugin/theme zip installations.

Best practices

  1. Always exclude /cache/, /node_modules/, or heavy logs.
  2. Use 9 for final backups, 1 for speed.
  3. Test archives with unzip -t.
  4. Encrypt sensitive data.
  5. Use split zips for >2GB backups.

Quick lab

  1. Navigate to /var/www/.

  2. Zip your html/ folder excluding cache.

    zip -r site.zip html/ -x "*/cache/*"

  3. Extract to /tmp/test/ and verify.

    unzip site.zip -d /tmp/test/


Cheat sheet

TaskCommand
Zip folderzip -r site.zip folder/
Max compressionzip -r -9 site.zip folder/
Encrypt zipzip -re secure.zip folder/
List contentsunzip -l archive.zip
Extract archiveunzip archive.zip
Extract to folderunzip archive.zip -d /path
Test integrityunzip -t archive.zip
Exclude fileszip -r archive.zip folder/ -x "*.log"

Mini quiz

  1. Which flag ensures recursive zipping of directories?
  2. How do you extract without directory structure?
  3. What command tests archive integrity?
  4. Why exclude cache when zipping WordPress?
  5. Which flag enables encryption with zip?


Path behavior and working directory

Whether you should cd first depends on the paths you want stored inside the ZIP.


Case 1: run zip from your current location

zip-recursive-current-location.sh
zip -r backup.zip /var/www/html/

  • Inside backup.zip, the path will be stored as var/www/html/....
  • When you unzip, it will recreate the full path relative to where you extract.

Result: Restores with folder hierarchy preserved.


Case 2: cd first into the target directory

zip-recursive-after-cd.sh
cd /var/www/
zip -r backup.zip html/

  • Inside backup.zip, the path will be html/....
  • When you unzip, it only restores from that level (no /var/www/ prefix).

Result: Cleaner archive, useful for migrations (esp. WordPress).


Best Practice for WordPress

  • If you’re packaging entire sites (e.g., /var/www/html/), it’s cleaner to cd into /var/www/ first.
  • If you want absolute clarity of source path (for system restore), don’t cd, zip with full paths.

Rule of Thumb:

  • Use cd first when creating portable/migratable .zip (like WordPress site or theme).
  • Skip cd if you want to preserve full directory structure for exact restore.

Targets: files and directories

Documentation often uses placeholders like file1 file2 dir/ to show that zip can accept multiple targets.

zip-syntax-template.sh
zip [options] archive.zip file1 file2 dir/

  • archive.zip is the output archive (a filename or an absolute path).
  • file1 file2 are individual files to include.
  • dir/ is a directory to include (use -r to include its contents).

What file1 file2 dir/ means

In the syntax:

zip-what-file1-file2-dir-means-093.sh
zip [options] archive.zip file1 file2 dir/

  • archive.zip → name of the zip file you are creating.
  • file1 file2 → individual files you want to include.
  • dir/ → directory (with all its contents, if r is used).

It means you can mix and match multiple files and directories in one archive.

Example:

zip-what-file1-file2-dir-means-095.sh
zip mybackup.zip index.php style.css wp-content/

This puts index.php, style.css, and the whole wp-content/ folder into mybackup.zip.


Running zip without a directory

Yes — you can zip only files if you want.

Example:

zip-running-zip-without-a-directory-097.sh
zip config.zip wp-config.php .htaccess

This will create config.zip with just those two files. No directory needed.


Zipping only a directory

If you want only a folder (not extra files), just point to that folder:

zip-zipping-only-a-directory-099.sh
zip -r site.zip /var/www/html/

Or, if you’ve cd into /var/www/:

zip-zipping-only-a-directory-101.sh
zip -r site.zip html/

That way, only the directory and its contents are zipped.


Where the ZIP file goes

  • By default, the .zip is created in your current working directory (the folder you are standing in when you run the command).

Example:

zip-where-the-zip-file-goes-103.sh
pwd
/home/user/
zip backup.zip file1.txt

Result → /home/user/backup.zip

  • If you want it somewhere else, give the full path:
zip-where-the-zip-file-goes-105.sh
zip -r /backups/site.zip /var/www/html

Result → /backups/site.zip


Rule of thumb for file1 file2 dir/

  • Single file → just put the filename.
  • Multiple files → list them one after another.
  • Directory → add r to recurse through it.
  • Mix → combine as you like.

Example Scenarios:

  • Only wp-config.php and .htaccess:

    zip configs.zip wp-config.php .htaccess

  • Only wp-content folder:

    zip -r wpcontent.zip wp-content/

  • Both files + folder together:

    zip -r fullbackup.zip wp-config.php .htaccess wp-content/


Absolute output paths are valid

The command below is standard syntax; it uses an absolute output path for the archive file and an absolute input path for the directory being archived.

zip-archive-absolute-paths.sh
zip -r /backups/site.zip /var/www/html

It is equivalent to the general template (the only difference is using full paths):

zip-syntax-template.sh
zip [options] archive.zip targets...


Standard syntax reminder

The general template is:

zip-syntax-template.sh
zip [options] archive.zip targets...

  • archive.zip → name (and optional path) of the zip file you want to create.
  • targets... → one or more files/directories you want to compress.

So, the “slot” for archive.zip can be just a filename (backup.zip) or a full path (/backups/site.zip).


Why the output path looks different

  • At first glance it looks like a path rather than just a filename.
  • But in fact, it’s just telling zip where to save the archive file.
  • Nothing changes in the syntax — you’re just using an absolute path (/backups/) instead of relying on the current directory.

In other words:

  • backup.zip = save the archive in the current directory.
  • /backups/site.zip = save the archive in /backups/ folder.

Both follow the same syntax — the difference is in where the resulting .zip lives.


Full breakdown of the command

zip-create-archive-absolute-paths.sh
zip -r /backups/site.zip /var/www/html

  • zip → command.
  • r → recurse through directories.
  • /backups/site.zipoutput archive path + filename.
  • /var/www/htmlinput directory to compress.

So this means:

"Create a zip named site.zip inside /backups/, containing everything inside /var/www/html recursively."


Relative vs absolute path examples

  • Standard teaching examples often show relative paths for clarity:

    zip-relative-path-example.sh
    zip -r site.zip html/

  • Production usage often uses absolute paths to avoid confusion:

    zip-absolute-path-example.sh
    zip -r /backups/site.zip /var/www/html

Both are syntactically valid. One is just shorter for learning, the other is safer for real ops.


Why use full paths?

  • Ensures you don’t accidentally save the archive in the wrong working directory.
  • Useful in automation scripts (cron jobs, backup scripts).
  • Keeps things predictable when running as root or via automation tools (where pwd may differ).

Conclusion:

Your command is absolutely standard — it’s just using absolute paths instead of relative ones. The "standard syntax" is written generically, while real-world commands often include explicit paths for clarity and safety.


If you are unsure how paths will be stored, run unzip -l on the archive and compare relative vs absolute patterns.

The command below is common on VPS hosts. Here is what each token means.

zip-why-use-full-paths-111.sh
zip -r /backups/site.zip /var/www/html


zip

  • The program itself.
  • Tells Linux: “I want to create or update a .zip archive.”

-r

  • Flag = recursive.
  • Means: “If the target is a directory, include everything inside it (subdirectories, files, etc.).”
  • Without r, only the top-level files would be added — subfolders would be skipped.

/backups/site.zip

This is NOT a source folder — it’s the output archive file.

  • /backups/ → the directory where the new archive will be saved.
  • site.zip → the filename of the archive being created.

Together → “Save the archive as /backups/site.zip.”

Important:

It does not mean you are zipping the /backups folder. It means you’re telling zip to put the new file there.


/var/www/html

This is the input source you are compressing.

  • It’s the path to your WordPress root (default for Apache/Nginx/OLS).
  • Everything inside /var/www/html will be added into the archive.

So this is the thing being zipped, not where the .zip is stored.


Example: output path vs input path

zip-backup-example.sh
zip -r /backups/site.zip /var/www/html

What happens:

  • /backups/site.zip is the output ZIP file (location and filename).
  • /var/www/html is the input directory being archived.

If /backups/ exists and you have write permissions, the resulting file is:

zip-backup-example-result.txt
/backups/site.zip

Restore drill: understand stored paths

ZIP is portable, but the paths stored inside the archive depend on how you ran zip. This matters when you extract.

Create the ZIP from a parent directory (preferred for restores)

This stores a clean, relative path (for example html/wp-config.php), which is easier to restore into a target directory.

zip-create-from-parent-dir.sh
cd /var/www
zip -r /backups/site.zip html

Create the ZIP using an absolute path

Most zip builds strip the leading / for safety, but they still store the full path components (for example var/www/html/wp-config.php).

zip-create-with-absolute-path.sh
zip -r /backups/site-abs.zip /var/www/html

Inspect what the ZIP will extract

Before you restore, list contents and confirm the top-level directory names.

unzip-list-archive-contents.sh
unzip -l /backups/site.zip | sed -n '1,25p'
unzip-list-archive-contents-absolute-paths.sh
unzip -l /backups/site-abs.zip | sed -n '1,25p'
warning

Never extract directly into /var/www/html until you have verified the stored paths. Extract into an empty staging directory first.

unzip-to-staging-directory.sh
rm -rf /tmp/restore-test
mkdir -p /tmp/restore-test
unzip /backups/site.zip -d /tmp/restore-test

Flag reference (zip)

FlagMeaningNotesExample
-rRecursiveRequired for directorieszip -r site.zip /var/www/html
-0Store onlyNo compression; fastestzip -r0 site.zip /var/www/html
-1 .. -9Compression level-1 fastest, -9 smallestzip -r9 site.zip /var/www/html
-qQuietUseful for cron logszip -rq site.zip /var/www/html
-xExclude patternsQuote patterns; test themzip -r site.zip /var/www/html -x '*.log' '*/cache/*'
-uUpdate (add changed/new files)Convenient but can hide drift; not ideal as your only backupzip -ru site.zip /var/www/html
-fFreshen (update existing entries only)Does not add new fileszip -rf site.zip /var/www/html
-FSFix archive / remove vanished entriesHelps keep an archive consistent during updateszip -rFS site.zip /var/www/html
-TTest after creatingAdds time, adds confidencezip -rT site.zip /var/www/html
-eEncrypt (prompt)Interactive; avoids putting secrets in your command historyzip -re secure.zip wp-config.php
-PEncrypt (inline password)Avoid for production: password is visible in shell history and process listszip -rP "$ZIP_PASSWORD" secure.zip wp-config.php
-mMove into archiveDeletes originals after adding; riskyzip -rm logs.zip /var/log/*.log

Flag reference (unzip)

FlagMeaningNotesExample
-lListInspect without extractingunzip -l site.zip
-tTestVerify integrityunzip -t site.zip
-d DIRExtract to directoryDirectory is created if missingunzip site.zip -d /tmp/restore-test
-nNever overwriteSafer default when testingunzip -n site.zip -d /tmp/restore-test
-oOverwrite without promptUse only in controlled restoresunzip -o site.zip -d /tmp/restore-test
-qQuietUseful in scriptsunzip -q site.zip -d /tmp/restore-test
-jJunk pathsFlattens directory structure; can overwrite files with the same nameunzip -j theme.zip -d /tmp/theme
-xExclude patternsFilter on extractunzip site.zip -d /tmp/restore-test -x '*/cache/*'

Common combinations (WordPress scenarios)

Full site snapshot (smaller ZIP)

zip-full-site-max-compression.sh
zip -r9 "/backups/site-$(date +%F).zip" /var/www/html

Use when transfer size matters more than CPU.

Fast pre-change snapshot (low CPU)

zip-full-site-fast-compression.sh
zip -r1 /backups/snapshot.zip /var/www/html

Use before risky changes (plugin update, theme change) when you want speed.

Skip caches and logs

zip-exclude-cache-and-logs.sh
zip -r9 /backups/site.zip /var/www/html -x '*/cache/*' '*.log'

Use to reduce archive size and avoid restoring junk files.

Integrity check (create + test)

zip-create-and-test.sh
zip -r9T /backups/site.zip /var/www/html
unzip -t /backups/site.zip

Use before uploading to offsite storage.

Flat extraction for a single bundle

unzip-flat-extract.sh
rm -rf /tmp/flat
mkdir -p /tmp/flat
unzip -j theme.zip -d /tmp/flat

Use for plugin/theme bundles when you explicitly want a flat output directory.

Safe restore without overwriting

unzip-safe-no-overwrite.sh
rm -rf /tmp/restore-test
mkdir -p /tmp/restore-test
unzip -n /backups/site.zip -d /tmp/restore-test

Use as your default restore drill. If the result looks correct, you can decide whether you want -o in a controlled restore.

warning

unzip -o overwrites files without prompting. Use it only when you are restoring into an empty directory or you have confirmed that overwriting is safe.

Timestamped filenames (shell substitution)

Adding a timestamp is a filename convention, not a zip option. Your shell expands $(date ...) before zip runs.

FormatExampleUse case
$(date +%F)2026-03-01Daily backups (human-readable)
$(date +%Y%m%d-%H%M)20260301-2245Hourly or pre-change snapshots
$(date +%Y%m%d_%H%M%S)20260301_224523High-frequency backups
zip-with-date-in-filename.sh
zip -r "/backups/site-$(date +%F).zip" /var/www/html

What the shell is (and why it matters)

The shell is the program that reads your command line and runs tools like zip, unzip, and date.

check-current-shell.sh
echo "$SHELL"
ps -p $$ -o comm=

In scripts, stick to a known shell (/bin/sh or /bin/bash) so substitutions like $(date ...) behave predictably.

Add host/user/unique IDs to filenames

Any command that prints text can be used inside $(...).

zip-filename-with-host-user-and-date.sh
zip -r "/backups/site-$(hostname)-$(whoami)-$(date +%Y%m%d-%H%M).zip" /var/www/html
zip-filename-with-uuid.sh
zip -r "/backups/site-$(date +%F)-$(uuidgen).zip" /var/www/html

Takeaway

  • Treat ZIP as a portability tool (client handoffs, cross-platform transfers).
  • Always inspect paths (unzip -l) and test restores into a staging directory.
  • Avoid inline passwords (zip -P) for anything sensitive.

WordPress-focused exclude patterns

Zipping /var/www/html as-is usually captures a lot of churn (caches, sessions, thumbnails) and sometimes sensitive artifacts (debug logs). Excluding the right paths makes ZIP backups smaller and restores cleaner.

info

Exclude patterns are evaluated by zip, and the shell can expand globs unless you quote them. As a habit, quote every -x pattern.

Common candidates:

PatternWhy excludeNotes
*/cache/*Cache directories can be rebuiltPlugin-specific paths vary
*/wflogs/*Wordfence logs change frequentlyConsider excluding only logs, not config
*/updraft/*Backup plugins can store their own archivesAvoid nesting backups inside backups
*.logDebug logs can grow quicklyKeep if you need forensic history
*.zip *.tar* *.zstPrevent recursion (archives inside archives)Reduces size, avoids confusion
zip-wordpress-with-excludes.sh
zip -r9 /backups/site.zip /var/www/html \
-x '*/cache/*' \
-x '*/wflogs/*' \
-x '*/updraft/*' \
-x '*.log' \
-x '*.zip' -x '*.tar*' -x '*.zst'

If you are unsure whether an exclude rule is safe, run a restore drill and verify the site in a staging environment.


Split large ZIPs for transfers

For very large sites, a single ZIP can be awkward to upload or store. zip can split archives into parts.

zip-split-archive-into-parts.sh
zip -r9 -s 2000m /backups/site-split.zip /var/www/html
ls -1 /backups/site-split.z*
zip-split-archive-result.txt
/backups/site-split.z01
/backups/site-split.z02
...
/backups/site-split.zip

To extract, point unzip at the .zip file (keep all parts in the same directory):

unzip-split-archive.sh
rm -rf /tmp/restore-test
mkdir -p /tmp/restore-test
unzip /backups/site-split.zip -d /tmp/restore-test
warning

Do not rename split parts unless you know the split naming conventions (.z01, .z02, ...). Missing or renamed parts will break extraction.


A practical backup script (ZIP + logs + retention)

This is a lightweight pattern for portable, human-readable ZIP backups. It intentionally does not try to be incremental; it produces a fresh archive each run.

zip-backup-script.sh
#!/usr/bin/env bash
set -euo pipefail

BACKUP_DIR="/backups"
SITE_ROOT="/var/www/html"
STAMP="$(date +%F)"
HOST="$(hostname)"
OUT="$BACKUP_DIR/site-${HOST}-${STAMP}.zip"
LOG="$BACKUP_DIR/site-${HOST}-${STAMP}.zip.log"

mkdir -p "$BACKUP_DIR"

{
echo "[$(date -Is)] starting zip backup"
echo "SITE_ROOT=$SITE_ROOT"
echo "OUT=$OUT"

# Run from a parent directory to store clean relative paths.
cd "$(dirname "$SITE_ROOT")"

nice -n 10 zip -r9T "$OUT" "$(basename "$SITE_ROOT")" \
-x '*/cache/*' \
-x '*/wflogs/*' \
-x '*/updraft/*' \
-x '*.log' \
-x '*.zip' -x '*.tar*' -x '*.zst'

echo "[$(date -Is)] verifying with unzip -t"
unzip -t "$OUT" >/dev/null

echo "[$(date -Is)] done"
} | tee "$LOG"

# Retention: keep 14 days of zip + log files.
find "$BACKUP_DIR" -maxdepth 1 -type f -name 'site-*.zip' -mtime +14 -delete
find "$BACKUP_DIR" -maxdepth 1 -type f -name 'site-*.zip.log' -mtime +14 -delete
warning

The find ... -delete lines permanently remove files. Test the script in a non-production directory first.


Restore checklist (safe-by-default)

Use this when you receive a ZIP from a client, when you download a ZIP from offsite storage, or when you want to practice a restore drill.

Inspect and test

restore-check-zip-integrity.sh
unzip -t /backups/site.zip
unzip -l /backups/site.zip | sed -n '1,40p'

Extract into an empty directory

restore-extract-into-empty-dir.sh
rm -rf /tmp/restore-test
mkdir -p /tmp/restore-test
unzip -n /backups/site.zip -d /tmp/restore-test

Confirm the extracted layout

You want to see a WordPress root with wp-config.php, wp-content/, and wp-includes/.

restore-check-wordpress-layout.sh
ls -la /tmp/restore-test
find /tmp/restore-test -maxdepth 3 -type f -name wp-config.php -print
find /tmp/restore-test -maxdepth 3 -type d -name wp-content -print

Compare counts before copying into place

restore-compare-file-counts.sh
echo "archive entries:"; unzip -l /backups/site.zip | wc -l
echo "extracted files:"; find /tmp/restore-test -type f | wc -l

Security notes for ZIP encryption

ZIP encryption exists, but in practice it is easy to use unsafely.

  • zip -e prompts for a password and avoids putting the secret in your history.
  • zip -P takes an inline password and is visible to other users via process listing and in your shell history.

If you want unattended encryption for offsite storage, a safer pattern is:

  1. create the ZIP without encryption
  2. encrypt the resulting artifact using a dedicated tool

Example with gpg (symmetric encryption):

encrypt-zip-with-gpg.sh
gpg --symmetric --cipher-algo AES256 --output /backups/site.zip.gpg /backups/site.zip
shred -u /backups/site.zip
warning

Only use shred on storage that supports secure overwrite semantics (traditional spinning disks). On SSDs and many filesystems, secure erase may not be guaranteed.


Troubleshooting

zip says "Nothing to do!"

This usually means the input path did not match anything (wrong directory, typo, or shell glob that expanded to nothing).

zip-troubleshoot-input-path.sh
ls -la /var/www/html
zip -r /tmp/test.zip /var/www/html

zip stores more path than you expected

If you used an absolute path, you may end up with entries like var/www/html/.... Create archives from a parent directory instead:

zip-fix-paths-by-cd.sh
cd /var/www
zip -r /backups/site.zip html

unzip reports "End-of-central-directory signature not found"

This usually indicates a corrupted download or a partial file.

unzip-troubleshoot-corrupt-download.sh
ls -lh /backups/site.zip
unzip -t /backups/site.zip

If the ZIP was transferred over the network, re-download and verify checksums if available.

unzip prompts about overwriting

Use an empty directory for test restores, or choose an explicit overwrite policy:

unzip-overwrite-policies.sh
# safer: never overwrite
unzip -n site.zip -d /tmp/restore-test

# controlled: overwrite
unzip -o site.zip -d /tmp/restore-test

Performance tips for busy VPS hosts

Large zips can be CPU and disk heavy.

zip-run-with-nice-and-ionice.sh
nice -n 10 ionice -c2 -n7 zip -r1 /backups/site.zip /var/www/html

Notes:

  • -r1 is often a good tradeoff when you care more about speed than the last few percent of compression.
  • nice and ionice reduce the impact on interactive users and production traffic.
  • If you repeatedly move large WordPress directories on Linux, prefer tar-based backups for better metadata fidelity.

Practice lab: create, verify, and restore

Use this as a repeatable drill you can run monthly.

zip-restore-drill.sh
set -e
rm -rf /tmp/restore-test
mkdir -p /tmp/restore-test

zip -r9T /tmp/site.zip /var/www/html
unzip -t /tmp/site.zip
unzip -n /tmp/site.zip -d /tmp/restore-test

find /tmp/restore-test -maxdepth 2 -type f -name wp-config.php -print

If this drill works reliably, you can trust the pattern when you have to restore under pressure.


Advanced usage patterns

Extract a single file (for quick recovery)

You can extract one file without restoring the entire archive.

unzip-extract-single-file.sh
rm -rf /tmp/restore-one
mkdir -p /tmp/restore-one
unzip /backups/site.zip 'html/wp-config.php' -d /tmp/restore-one
ls -la /tmp/restore-one/html/wp-config.php

If you are not sure about the stored path (html/... vs var/www/html/...), list the archive first:

unzip-find-stored-path.sh
unzip -l /backups/site.zip | rg -n 'wp-config\.php$'

Extract only a subtree

This is useful when you only need wp-content/uploads/.

unzip-extract-only-uploads.sh
rm -rf /tmp/uploads-only
mkdir -p /tmp/uploads-only
unzip /backups/site.zip 'html/wp-content/uploads/*' -d /tmp/uploads-only

Create a ZIP without the top-level directory

Sometimes you want the archive to contain the WordPress root files directly (not wrapped inside html/).

zip-create-without-top-level-dir.sh
cd /var/www/html
zip -r /backups/site-root.zip .
warning

zip -r ... . stores paths relative to the current directory, which is convenient, but it also makes it easier to accidentally run the command from the wrong folder. Double-check pwd before you run it.

Update an existing ZIP (use with caution)

zip -u is convenient, but it can hide drift if files are deleted on disk but remain in the archive. If you must update an archive, consider -FS as well.

zip-update-existing-archive.sh
cd /var/www
zip -ru /backups/site.zip html
zip -rFS /backups/site.zip html
unzip -t /backups/site.zip

Metadata and permissions caveats

ZIP is designed for portability, not perfect Linux metadata fidelity.

  • Ownership and some permission details may not be preserved the way you expect on restore.
  • Symlinks may be stored differently depending on options and zip build.
  • For strict Linux-to-Linux restores (ownership, permissions, special files), prefer tar-based workflows.

If you restore a WordPress tree into /var/www/html, you typically need to fix ownership afterward:

restore-fix-permissions-wordpress.sh
sudo chown -R www-data:www-data /var/www/html
sudo find /var/www/html -type d -exec chmod 755 {} \;
sudo find /var/www/html -type f -exec chmod 644 {} \;
warning

Do not blindly apply permission changes to shared hosting paths or custom PHP-FPM users. Use the ownership model that matches your webserver configuration.


Quick reference

zip-quick-reference.sh
# Create
zip -r /backups/site.zip /var/www/html

# Create (smallest)
zip -r9 /backups/site.zip /var/www/html

# Create (fastest)
zip -r1 /backups/site.zip /var/www/html

# List
unzip -l /backups/site.zip

# Test
unzip -t /backups/site.zip

# Extract to a directory
unzip /backups/site.zip -d /tmp/restore-test

Common mistakes

Running zip -r from the wrong directory

If you run the command from an unexpected directory, you may end up storing the wrong root (or including unrelated files). Always sanity-check:

zip-sanity-check-before-running.sh
pwd
ls -la

Using -j on a full site archive

-j (junk paths) flattens directory structure. It is useful for specific bundles, but it is a foot-gun for full WordPress backups because many files share the same basename.

If you see -j in a backup script, make sure it is intentional and scoped.