A static-site deploy has no build step to fail and no framework to misconfigure, which is why the failures it does have are so quiet. The archive builds, the copy succeeds, the extraction reports nothing, and the site is subtly wrong. Both faults in this guide came from one deploy of a site I run, and both originate on the Mac side, before the server is ever involved.
Step 1. Build the archive with the two flags
macOS stores metadata, Finder tags, quarantine flags and resource forks, as extended attributes on files. When the stock tar on macOS archives a file that carries any of them, it writes a sidecar entry named ._filename alongside the real one, in AppleDouble format, so the metadata survives on filesystems that cannot hold it. A Linux server cannot use these files and should never receive them. On my deploy, nineteen of them rode along inside the archive and were extracted into the web root as ordinary files, where a crawler is happy to find them.
Build the archive like this instead:
COPYFILE_DISABLE=1 tar --no-xattrs -czf site.tar.gz -C build .
COPYFILE_DISABLE=1 tells macOS tar not to emit AppleDouble entries at all. --no-xattrs tells it not to record extended attributes in the archive. The two overlap on purpose; either alone covers most cases, both together cover the versions of tar where one of them is ignored. -C build . archives the contents of the build directory rather than the directory itself, so extraction does not create an extra level.
Step 2. Verify the archive is clean before it leaves the Mac
tar -tzf site.tar.gz | grep -c '/\._'
The count must be 0, and if it is not, the environment variable did not reach tar, which happens when the command runs through a wrapper or a Makefile that does not pass the environment along. Fix that before shipping; deleting sidecars on the server is the losing version of this game, because the next deploy puts them back.
Step 3. Write a manifest so the server can prove what landed
find build -type f -exec shasum -a 256 {} + | sed 's| build/| |' > MANIFEST.sha256
macOS ships shasum; Linux ships sha256sum. The formats match, so the server can verify with the standard tool. A manifest turns “the copy probably worked” into a checked fact, and it catches the truncated upload that a flaky connection produces once a year.
Step 4. Extract to staging, never straight into the web root
On the server:
mkdir -p /tmp/deploy-staging && tar -xzf site.tar.gz -C /tmp/deploy-staging
cd /tmp/deploy-staging && sha256sum -c /path/to/MANIFEST.sha256 | grep -v ': OK$' || echo "all checksums OK"
Every line should be OK. A failed line names the exact file that arrived wrong, which beats discovering it as a broken page.
Step 5. Fix the permissions the Mac gave the files
This is the second fault from the deploy that prompted this guide. Files created by some Mac applications, and files that spent time in directories with restrictive umasks, arrive as mode 600: readable by their owner and nobody else. Extraction preserves those modes, and the web server, which runs as its own unprivileged user, cannot read the files and answers 403 for exactly those pages while serving their neighbours normally, which makes the failure look random.
find /tmp/deploy-staging -type f -exec chmod 644 {} +
find /tmp/deploy-staging -type d -exec chmod 755 {} +
Run it on staging every deploy, unconditionally. It costs nothing when the modes were already right.
Step 6. Move into place and test
rsync -a --delete /tmp/deploy-staging/ /var/www/site/
nginx -t && systemctl reload nginx
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/some-page
If you use rsync from the Mac directly instead of an archive, the same two faults arrive by a different road; add --no-xattrs there too, and know that -a preserves the Mac’s modes, so the chmod pass on the server stays in the routine. Zip archives made on macOS carry a __MACOSX/ directory that is the same sidecar problem wearing a different name.
Check it worked
Four numbers, all cheap: find /var/www/site -name '._*' | wc -l returns 0; sha256sum -c MANIFEST.sha256 reports every file OK; find /var/www/site -type f ! -perm 644 | wc -l returns 0; and a curl of a page that previously returned 403 returns 200. Keep the four lines in the deploy script so they run every time instead of the time you remember.
Where I could be wrong
The advice assumes nginx or Apache running as a non-root user reading a static tree. A setup that serves through an application process with different ownership needs its own permission target, and 644 on a file containing secrets would be the opposite of a fix; a static marketing site has no such files.
GNU tar installed through Homebrew behaves differently from the BSD tar macOS ships, and does not write AppleDouble entries in the first place. If your Mac uses gtar, the sidecar half of this guide is solved already and the permissions half still applies.
Sources
- Apple, tar(1) manual page, COPYFILE_DISABLE
- Apple Developer, About extended attributes and AppleDouble format, xattr(1)
- GNU coreutils, sha256sum documentation
- nginx documentation, ngx_http_core_module