~ / insights / guides

Guide · September 2026

Two Ways a Mac Breaks a Linux Deploy.

I shipped a static site from a Mac to an Ubuntu server and got nineteen junk files and a batch of 403s. Both faults came from the Mac, and both are two flags to prevent.

Amit TiwariGuide5 min read

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

How the two faults travel Mac: files with extended attributes and mode 600 tar without flags: ._ sidecars written, modes preserved Server: extract into the web root nginx: ._ files served as pages, 600 files answer 403
Figure 1. Neither step reports an error. The site is simply subtly wrong.

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

The whole fix is two flags, one chmod pass and a manifest Before tar -czf site.tar.gz build 19 ._ sidecar files in the archive HTML files at mode 600 No way to prove what landed After COPYFILE_DISABLE=1 tar –no-xattrs -czf tar -tzf … | grep -c '/\._' returns 0 find … -type f -exec chmod 644 {} + on staging sha256sum -c MANIFEST.sha256 every file OK
Figure 2. Left is what the first deploy ran. Right is the routine now.

Interactive: build the deploy commands for your setup. Needs JavaScript.

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

The four lines that live in the deploy script CHECK COMMAND MUST RETURN No sidecars find /var/www/site -name '._*' | wc -l 0 Everything arrived intact sha256sum -c MANIFEST.sha256 every line OK Readable by nginx find /var/www/site -type f ! -perm 644 | wc -l 0 A page that was 403 curl -s -o /dev/null -w '%{http_code}' URL 200
Figure 3. Run every time, not the time you remember.

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

How to cite this guide

Amit Tiwari (2026). Two Ways a Mac Breaks a Linux Deploy. Guide, September 2026. amittiwari.net. https://amittiwari.net/guides/two-ways-a-mac-breaks-a-linux-deploy

Discuss in the community ↗