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.
- 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.
sudo apt update
sudo apt install -y zip unzip
which zip
zip -v
zip --version
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
taron Linux.
Syntax breakdown
zip Syntax
zip [OPTIONS] TARGET_LOCATION/OUTPUT_FILE.zip INPUT_FILES_OR_DIRECTORIES
| Segment | Description | Example | Notes |
|---|---|---|---|
zip | The command to create or update a zip archive. | zip | Must be installed (sudo apt install zip). |
[OPTIONS] | Flags that modify behavior (e.g., recursive, compression level, exclude). | -r, -9, -x | Combine as needed. |
OUTPUT_FILE.zip | Destination path and filename of the archive. | /root/backup.zip | Should end with .zip. Can include full or relative path. |
INPUT_FILES_OR_DIRECTORIES | One or more files or directories to include. | /var/www/html, index.php, config/ | Can be absolute or relative paths. |
unzip Syntax
unzip [OPTIONS] ARCHIVE.zip [-d TARGET_DIRECTORY]
| Segment | Description | Example | Notes |
|---|---|---|---|
unzip | Command used to extract .zip archives. | unzip | Must be installed (sudo apt install unzip). |
[OPTIONS] | Modifiers to control extraction behavior. | -o, -l, -t, -d | Control overwrite, listing, testing, etc. |
ARCHIVE.zip | The name (and path) of the ZIP file to extract. | /root/html_backup.zip | Can use absolute or relative path. |
-d TARGET_DIRECTORY | Directory where files will be extracted. | -d /var/www/html_restore | Optional — default is current directory. |
Options and flags
| Option | Meaning | Example | Notes |
|---|---|---|---|
-r | Recursively include subdirectories. | zip -r site.zip /var/www/html | Essential for folders. |
-9 | Use maximum compression. | zip -9 backup.zip file.txt | Slower but smaller file. |
-q | Quiet mode (no output). | zip -rq backup.zip html | Useful for cron jobs. |
-x | Exclude files or patterns. | zip -r backup.zip html -x "*.log" "*.cache/*" | Handy for skipping caches/logs. |
-u | Update changed files in an existing zip. | zip -ru backup.zip html | Keeps archive fresh without recreating. |
-f | Freshen (update existing entries only). | zip -rf backup.zip html | Doesn’t add new files. |
-d | Delete entries from an existing archive. | zip -d backup.zip *.tmp | Removes unwanted files. |
-m | Move files into zip (delete originals). | zip -rm backup.zip old_logs/ | Use with caution! |
-j | Junk paths — don’t store directory structure. | zip -rj backup.zip html/* | Flattens files (dangerous for duplicates). |
-e | Encrypt with password. | zip -re backup.zip html | Prompts for password. |
-T | Test archive integrity after creation. | zip -rT backup.zip html | Good for verification. |
Core syntax
1. Create a ZIP archive of a directory
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 [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 -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 -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 -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 -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 -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
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
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
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
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 [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 -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
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 file.zip index.php
adding: index.php (deflated 20%)
Use Case: Quickly package a config file.
Benefit: Lightweight transfer.
Create a zip of a directory recursively
zip -r site.zip /var/www/html/
adding: html/ (stored 0%)
adding: html/wp-config.php (deflated 42%)
Use Case: Backup full WordPress site.
Benefit: Single portable file.
Maximum compression
zip -r -9 site_max.zip /var/www/html
adding: html/wp-content/plugins/ (deflated 75%)
Use Case: Reduce size for slow transfers.
Benefit: Saves bandwidth.
Quiet mode (no logs)
zip -rq quiet.zip *
(no output)
Use Case: Clean automation scripts.
Benefit: No cluttered logs.
Encrypt a zip
zip -re secure.zip wp-content/
Enter password:
Verify password:
adding: wp-content/...
Use Case: Protect sensitive files.
Benefit: Safer sharing.
Exclude a directory
zip -r site_no_cache.zip /var/www/html -x "*/cache/*"
skipping: html/wp-content/cache/
Use Case: Ignore cache.
Benefit: Smaller, cleaner archive.
List contents of a zip
unzip -l site.zip
Length Method Size Name
------ ------ ---- ----
23456 Defl:N 6789 wp-config.php
Use Case: Verify before extracting.
Benefit: Avoids surprises.
Extract to current directory
unzip site.zip
extracting: wp-config.php
Use Case: Quick restore.
Benefit: Default simple use.
Extract to a custom directory
unzip site.zip -d /tmp/restore/
extracting: /tmp/restore/wp-content/index.php
Use Case: Testing restore.
Benefit: Keeps clean environment.
Overwrite existing files
unzip -o site.zip
replace index.php? [y]es, [n]o, [A]ll:
inflating: index.php
Use Case: Auto updates.
Benefit: Saves manual confirmation.
Extract while ignoring paths
unzip -j plugins.zip
extracting: plugin1.php
Use Case: Only files, no dirs.
Benefit: Clean flat output.
Test ZIP integrity
unzip -t site.zip
No errors detected in compressed data.
Use Case: Validate backups.
Benefit: Prevents corrupt restores.
Create multiple ZIPs from a file list
zip files.zip $(cat filelist.txt)
adding: index.html
adding: style.css
Use Case: Controlled packaging.
Benefit: Customizable.
Add files to an existing archive
zip update.zip newfile.txt
updating: newfile.txt (stored 0%)
Use Case: Append logs.
Benefit: No need to rebuild.
Extract only a specific file
unzip site.zip wp-config.php
extracting: wp-config.php
Use Case: Recover only config.
Benefit: Saves time.
Delete a file inside a ZIP
zip -d site.zip readme.html
deleting: readme.html
Use Case: Remove junk.
Benefit: Cleaner archive.
Create a split ZIP (multi-part)
zip -r -s 100m bigsite.zip /var/www/html
creating: bigsite.z01
creating: bigsite.z02
Use Case: Upload in parts.
Benefit: Bypass file size limits.
Extract multiple ZIPs in a batch
for z in *.zip; do unzip -o "$z" -d extracted/; done
Use Case: Bulk plugin installs.
Benefit: Automation.
Zip the current folder only
zip -r current.zip .
adding: ./index.php
Use Case: Snapshot folder.
Benefit: Quick backup.
Combine zip with move
zip -r site.zip /var/www/html && mv site.zip /backups/
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
- Always exclude
/cache/,/node_modules/, or heavy logs. - Use
9for final backups,1for speed. - Test archives with
unzip -t. - Encrypt sensitive data.
- Use split zips for >2GB backups.
Quick lab
-
Navigate to
/var/www/. -
Zip your
html/folder excluding cache.zip -r site.zip html/ -x "*/cache/*" -
Extract to
/tmp/test/and verify.unzip site.zip -d /tmp/test/
Cheat sheet
| Task | Command |
|---|---|
| Zip folder | zip -r site.zip folder/ |
| Max compression | zip -r -9 site.zip folder/ |
| Encrypt zip | zip -re secure.zip folder/ |
| List contents | unzip -l archive.zip |
| Extract archive | unzip archive.zip |
| Extract to folder | unzip archive.zip -d /path |
| Test integrity | unzip -t archive.zip |
| Exclude files | zip -r archive.zip folder/ -x "*.log" |
Mini quiz
- Which flag ensures recursive zipping of directories?
- How do you extract without directory structure?
- What command tests archive integrity?
- Why exclude cache when zipping WordPress?
- 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 -r backup.zip /var/www/html/
- Inside
backup.zip, the path will be stored asvar/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
cd /var/www/
zip -r backup.zip html/
- Inside
backup.zip, the path will behtml/.... - 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 tocdinto/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
cdfirst when creating portable/migratable.zip(like WordPress site or theme). - Skip
cdif 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 [options] archive.zip file1 file2 dir/
archive.zipis the output archive (a filename or an absolute path).file1 file2are individual files to include.dir/is a directory to include (use-rto include its contents).
What file1 file2 dir/ means
In the syntax:
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, ifris used).
It means you can mix and match multiple files and directories in one archive.
Example:
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 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 -r site.zip /var/www/html/
Or, if you’ve cd into /var/www/:
zip -r site.zip html/
That way, only the directory and its contents are zipped.
Where the ZIP file goes
- By default, the
.zipis created in your current working directory (the folder you are standing in when you run the command).
Example:
pwd
/home/user/
zip backup.zip file1.txt
Result → /home/user/backup.zip
- If you want it somewhere else, give the full path:
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
rto 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 -r /backups/site.zip /var/www/html
It is equivalent to the general template (the only difference is using full paths):
zip [options] archive.zip targets...
Standard syntax reminder
The general template is:
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
zipwhere 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 -r /backups/site.zip /var/www/html
zip→ command.r→ recurse through directories./backups/site.zip→ output archive path + filename./var/www/html→ input 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.shzip -r site.zip html/ -
Production usage often uses absolute paths to avoid confusion:
zip-absolute-path-example.shzip -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
rootor via automation tools (wherepwdmay 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 -r /backups/site.zip /var/www/html
zip
- The program itself.
- Tells Linux: “I want to create or update a
.ziparchive.”
-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/htmlwill 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 -r /backups/site.zip /var/www/html
What happens:
/backups/site.zipis the output ZIP file (location and filename)./var/www/htmlis the input directory being archived.
If /backups/ exists and you have write permissions, the resulting file is:
/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.
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 -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 -l /backups/site.zip | sed -n '1,25p'
unzip -l /backups/site-abs.zip | sed -n '1,25p'
Never extract directly into /var/www/html until you have verified the stored paths. Extract into an empty staging directory first.
rm -rf /tmp/restore-test
mkdir -p /tmp/restore-test
unzip /backups/site.zip -d /tmp/restore-test
Flag reference (zip)
| Flag | Meaning | Notes | Example |
|---|---|---|---|
-r | Recursive | Required for directories | zip -r site.zip /var/www/html |
-0 | Store only | No compression; fastest | zip -r0 site.zip /var/www/html |
-1 .. -9 | Compression level | -1 fastest, -9 smallest | zip -r9 site.zip /var/www/html |
-q | Quiet | Useful for cron logs | zip -rq site.zip /var/www/html |
-x | Exclude patterns | Quote patterns; test them | zip -r site.zip /var/www/html -x '*.log' '*/cache/*' |
-u | Update (add changed/new files) | Convenient but can hide drift; not ideal as your only backup | zip -ru site.zip /var/www/html |
-f | Freshen (update existing entries only) | Does not add new files | zip -rf site.zip /var/www/html |
-FS | Fix archive / remove vanished entries | Helps keep an archive consistent during updates | zip -rFS site.zip /var/www/html |
-T | Test after creating | Adds time, adds confidence | zip -rT site.zip /var/www/html |
-e | Encrypt (prompt) | Interactive; avoids putting secrets in your command history | zip -re secure.zip wp-config.php |
-P | Encrypt (inline password) | Avoid for production: password is visible in shell history and process lists | zip -rP "$ZIP_PASSWORD" secure.zip wp-config.php |
-m | Move into archive | Deletes originals after adding; risky | zip -rm logs.zip /var/log/*.log |
Flag reference (unzip)
| Flag | Meaning | Notes | Example |
|---|---|---|---|
-l | List | Inspect without extracting | unzip -l site.zip |
-t | Test | Verify integrity | unzip -t site.zip |
-d DIR | Extract to directory | Directory is created if missing | unzip site.zip -d /tmp/restore-test |
-n | Never overwrite | Safer default when testing | unzip -n site.zip -d /tmp/restore-test |
-o | Overwrite without prompt | Use only in controlled restores | unzip -o site.zip -d /tmp/restore-test |
-q | Quiet | Useful in scripts | unzip -q site.zip -d /tmp/restore-test |
-j | Junk paths | Flattens directory structure; can overwrite files with the same name | unzip -j theme.zip -d /tmp/theme |
-x | Exclude patterns | Filter on extract | unzip site.zip -d /tmp/restore-test -x '*/cache/*' |
Common combinations (WordPress scenarios)
Full site snapshot (smaller ZIP)
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 -r1 /backups/snapshot.zip /var/www/html
Use before risky changes (plugin update, theme change) when you want speed.
Skip caches and logs
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 -r9T /backups/site.zip /var/www/html
unzip -t /backups/site.zip
Use before uploading to offsite storage.
Flat extraction for a single bundle
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
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.
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.
| Format | Example | Use case |
|---|---|---|
$(date +%F) | 2026-03-01 | Daily backups (human-readable) |
$(date +%Y%m%d-%H%M) | 20260301-2245 | Hourly or pre-change snapshots |
$(date +%Y%m%d_%H%M%S) | 20260301_224523 | High-frequency backups |
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.
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 -r "/backups/site-$(hostname)-$(whoami)-$(date +%Y%m%d-%H%M).zip" /var/www/html
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.
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:
| Pattern | Why exclude | Notes |
|---|---|---|
*/cache/* | Cache directories can be rebuilt | Plugin-specific paths vary |
*/wflogs/* | Wordfence logs change frequently | Consider excluding only logs, not config |
*/updraft/* | Backup plugins can store their own archives | Avoid nesting backups inside backups |
*.log | Debug logs can grow quickly | Keep if you need forensic history |
*.zip *.tar* *.zst | Prevent recursion (archives inside archives) | Reduces size, avoids confusion |
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 -r9 -s 2000m /backups/site-split.zip /var/www/html
ls -1 /backups/site-split.z*
/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):
rm -rf /tmp/restore-test
mkdir -p /tmp/restore-test
unzip /backups/site-split.zip -d /tmp/restore-test
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.
#!/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
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
unzip -t /backups/site.zip
unzip -l /backups/site.zip | sed -n '1,40p'
Extract into an empty directory
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/.
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
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 -eprompts for a password and avoids putting the secret in your history.zip -Ptakes 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:
- create the ZIP without encryption
- encrypt the resulting artifact using a dedicated tool
Example with gpg (symmetric encryption):
gpg --symmetric --cipher-algo AES256 --output /backups/site.zip.gpg /backups/site.zip
shred -u /backups/site.zip
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).
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:
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.
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:
# 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.
nice -n 10 ionice -c2 -n7 zip -r1 /backups/site.zip /var/www/html
Notes:
-r1is often a good tradeoff when you care more about speed than the last few percent of compression.niceandionicereduce 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.
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.
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 -l /backups/site.zip | rg -n 'wp-config\.php$'
Extract only a subtree
This is useful when you only need wp-content/uploads/.
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/).
cd /var/www/html
zip -r /backups/site-root.zip .
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.
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
zipbuild. - 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:
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 {} \;
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
# 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:
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.