Apprentice

Deploying Astro 6 to Cloudron Surfer

Plane of Infrastructure: Cloudron Surfer

This is the first post in the Plane of Infrastructure series, covering self-hosting, Cloudron, and the tools that run beneath the surface of this blog.

Cloudron is a self-hosted platform for running web applications. It handles SSL certificates, backups, user management, and updates. Surfer is Cloudron’s built-in static file server. You point a domain at it, upload your files, and it serves them over HTTPS.

This blog runs on Surfer. Getting it there was not as simple as it should have been.

The surfer CLI Lies By Omission

The surfer CLI is the official tool for uploading files to a Surfer instance. The command looks straightforward:

terminal
surfer put dist/* /

This command will appear to succeed. It will not throw an error. It will not warn you. But it silently skips two categories of files:

  1. Root-level HTML files (index.html, 404.html)
  2. New hashed assets in _astro/ that do not already exist on the server

The result is a deploy that looks successful but leaves the site broken or stale. CSS and JavaScript files 404. The homepage serves the old version. You spend an hour checking your build output before realising the upload tool is the problem.

The Fix: curl and WebDAV

Surfer exposes a WebDAV endpoint at /_webdav/. Uploading via curl bypasses the CLI entirely and reliably uploads every file type.

scripts/deploy.sh
# Upload a single file
curl -T file "https://wanderingmonster.dev/_webdav/path/to/file" \
-u "ignored:${SURFER_TOKEN}"
# Create a directory (MKCOL)
curl -X MKCOL \
"https://wanderingmonster.dev/_webdav/_astro/" \
-u "ignored:${SURFER_TOKEN}"

The username field is ignored by Surfer’s WebDAV. You can put anything there. The token is what matters.

The full deploy script walks the dist/ directory, creates all directories with MKCOL first, then uploads every file with PUT. The script is at scripts/deploy.sh in this repo.

Getting the Token

The Surfer WebDAV token is not your Cloudron account password. This is the mistake everyone makes the first time.

The token is a per-app API token found in the Surfer admin panel:

  1. Go to https://your-site/_admin/
  2. Log in with your Cloudron credentials
  3. Find the token in the settings section

Set it as an environment variable:

terminal
export SURFER_TOKEN="your-token-here"
pnpm run deploy

Access Control

One more thing that will trip you up: the Surfer app must have access control set to Public in the Cloudron dashboard. If it is set to anything else, every page returns a 401 Unauthorized response.

This is configured in the Cloudron dashboard under the app settings, not in the Surfer admin panel. They are different interfaces for different things.

The Deploy Workflow

With the WebDAV script in place, the workflow is:

terminal
export SURFER_TOKEN="your-token"
pnpm run deploy

Which runs:

  1. astro build to generate the static site into dist/
  2. pagefind --site dist to generate the search index
  3. bash scripts/deploy.sh to upload everything via WebDAV

After deploying, verify with a hard refresh (Ctrl+Shift+R) and check that /_astro/ CSS and JS files load without 404s.

Verifying a Deploy

A quick checklist after every deploy:

  1. Hard refresh the site in your browser
  2. Check that /_astro/ CSS and JS files load (open devtools network tab)
  3. Check that root pages (/, /blog, /rss.xml) render correctly
  4. If CSS is missing, the hashed _astro/ files were not uploaded

If step 4 fails and you used the WebDAV script, check that SURFER_TOKEN is correct and that the app has public access control enabled.

Lessons Learned

Every lesson in this post was learned the hard way. The surfer put silent skip cost several hours of debugging. The token confusion cost another round. The access control 401 was the final boss.

Self-hosting is worth it for ownership and privacy. But the tooling has rough edges. Document them, script around them, and do not trust a CLI that succeeds silently.

The deploy script in this repo is the result. It works. Use it.