# How Do I Preview a Hugo or Astro Site on an HTTPS URL in a Sandbox?

> Run hugo server with --baseURL set to the hosted URL, --appendPort=false and --liveReloadPort 443, or run astro dev with server.allowedHosts set to .box.sandboxer.com, then sbx host the port. Live reload works through the private URL. A 300-page build took about 0.15 s in Hugo and 4.2 s in Astro.

Source: https://sandboxer.com/guides/preview-a-hugo-or-astro-site-in-a-sandbox. Updated 2026-10-27.

**Short answer:** Run hugo server with --baseURL set to the hosted URL, --appendPort=false and --liveReloadPort 443, or run astro dev with server.allowedHosts set to .box.sandboxer.com, then sbx host the port. Live reload works through the private URL. A 300-page build took about 0.15 s in Hugo and 4.2 s in Astro.

How to host a port is covered in [give a sandbox port an HTTPS URL](/guides/give-a-sandbox-port-an-https-url), and per-PR previews in [a preview URL for every pull request](/guides/get-a-preview-url-for-every-pull-request). This page covers what Hugo and Astro need on top of that. Their dev servers assume they're on `localhost`, so behind an HTTPS proxy they break in ways that don't show up on your laptop.

## What we ran

Everything below ran on production on October 27, 2026, in one `small` sandbox (2 vCPU, 4 GB RAM, $0.018/hour). Times are from one sandbox. Build times are three runs. Tokens are replaced with `<token>`.

| Step | Result |
| - | - |
| `sbx new --type small` | 3.2 s to ready |
| Install Hugo v0.168.0 extended from the GitHub `.deb` | worked (not timed) |
| `hugo --minify`, 338 pages | 150, 175, 152 ms (Hugo's own "Total in") |
| `npm create astro@latest` (minimal template, with install) | 26.3 s |
| Upgrade Node.js 22 to 24 for Astro 7.4.2 | 13 s |
| `astro build`, 301 pages | 4.22 s reported, 4.6–5.5 s wall clock with `npx` |
| Edit a file to the reload message on the laptop, through the private URL | Hugo 357 ms, Astro 227 ms (each includes a ~200 ms `sbx exec` round trip) |
| `sbx stop --wait`, then `sbx resume` | 2.2 s and 1.3 s, `memoryRestored: true`, both URLs `200` |

## Hugo

We installed the extended edition from the `.deb` on Hugo's [GitHub releases](https://github.com/gohugoio/hugo/releases), one of the methods in its [Linux install docs](https://gohugo.io/installation/linux/). Ubuntu's `apt` package is usually older.

We saved these lines as `hugo-setup.sh` in the sandbox (`sbx new --name blog --type small` gave us `sb_5698hpwhgl`, so the commands below use `id=sb_5698hpwhgl`) and ran it with `sbx exec $id --timeout 600 "bash hugo-setup.sh"`:

```bash
set -e
V=$(curl -fsSL https://api.github.com/repos/gohugoio/hugo/releases/latest | jq -r .tag_name | sed 's/^v//')
curl -fsSLo /tmp/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${V}/hugo_extended_${V}_linux-amd64.deb
sudo dpkg -i /tmp/hugo.deb >/dev/null
hugo version
hugo new site blog >/dev/null && cd blog && hugo new theme demo >/dev/null && echo "theme = 'demo'" >> hugo.toml
```

```text
hugo v0.168.0-e27a6760c5547237f87d353aebe0580feadb20f4+extended linux/amd64 BuildDate=2026-10-13T16:42:50Z VendorInfo=gohugoio
```

### What goes wrong with the defaults

We first started `hugo server --bind 0.0.0.0 --port 1313` and hosted port 1313. The page loaded through the private URL. Two things were broken:

```bash
curl -s -b jar https://ample-vital-ember-1313.box.sandboxer.com/ | grep -o '<script[^>]*livereload[^>]*>'
curl -s -b jar https://ample-vital-ember-1313.box.sandboxer.com/index.xml | grep -o '<link>[^<]*</link>' | head -2
```

```text
<script src="/livereload.js?mindelay=10&amp;v=2&amp;port=1313&amp;path=livereload" data-no-instant defer>
<link>http://localhost:1313/</link>
<link>http://localhost:1313/posts/posts-107/</link>
```

- **Live reload never connects.** The script opens its WebSocket on port 1313, but the hosted URL only answers on 443. Our test of `https://ample-vital-ember-1313.box.sandboxer.com:1313/` got `Could not connect to server`.
- **Absolute URLs point at `localhost:1313`.** The RSS feed, the sitemap and anything a theme builds with `.Permalink` or `absURL` link to your own laptop. Relative links in the page work, so this is easy to miss until you click a canonical link or test a feed.

### The fix: three flags

The [`hugo server`](https://gohugo.io/commands/hugo_server/) flags that fix it are `--baseURL` (the hosted URL), `--appendPort=false` (keep `:1313` out of generated links) and `--liveReloadPort 443`, which Hugo's help describes as the setting for HTTPS proxies. `$SANDBOXER_SUBDOMAIN` is set inside every sandbox, so the unit can be written without copying the name. A systemd unit keeps the server running after a reboot:

```bash
sbx exec $id 'sudo tee /etc/systemd/system/hugo-preview.service >/dev/null <<EOF
[Unit]
Description=Hugo preview server
After=network-online.target

[Service]
User=user
WorkingDirectory=/home/user/blog
ExecStart=/usr/local/bin/hugo server --bind 0.0.0.0 --port 1313 --baseURL https://${SANDBOXER_SUBDOMAIN}-1313.box.sandboxer.com/ --appendPort=false --liveReloadPort 443
Restart=always

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload && sudo systemctl enable --now hugo-preview'
sbx host $id 1313 --title Hugo
```

```text
https://ample-vital-ember-1313.box.sandboxer.com?_token=<token>
```

After that, the same two checks:

```text
<script src="/livereload.js?mindelay=10&amp;v=2&amp;port=443&amp;path=livereload" data-no-instant defer>
<link>https://ample-vital-ember-1313.box.sandboxer.com/</link>
<link>https://ample-vital-ember-1313.box.sandboxer.com/posts/posts-107/</link>
```

To check live reload without a browser, we opened `wss://…-1313.box.sandboxer.com/livereload` from a laptop with the `__sbx_auth` cookie (see the [WebSocket section of the hosting guide](/guides/give-a-sandbox-port-an-https-url) for how to get it), sent the LiveReload `hello`, then edited a post with `sbx exec`:

```text
pre-edit {"command":"hello","protocols":["http://livereload.com/protocols/official-7"],"serverName":"Hugo"}
edit sent after 201 ms
+357ms {"command":"reload","path":"/x.js","originalPath":"","liveCSS":true,"liveImg":true}
```

The `reload` arrived about 150 ms after the edit command returned, and the page then served the new text. In a browser, opening the token link sets the cookie and live reload works with no extra steps. The Hugo server process used about 100 MB of RAM with 300 posts.

## Astro

We followed Astro's [install guide](https://docs.astro.build/en/install-and-setup/) with the minimal template:

```bash
sbx exec $id --timeout 600 'npm create --yes astro@latest site -- --template minimal --install --no-git --yes --skip-houston'
```

That took 26.3 s and installed Astro 7.4.2. The next command failed:

```text
Node.js v22.23.3 is not supported by Astro!
Please upgrade Node.js to a supported version: ">=22.23.4"
```

Astro 7.4.2's `package.json` asks for Node.js `>=22.23.4`, one patch release above the Node.js 22 in the sandbox image on that day ([what's preinstalled](/guides/whats-preinstalled-in-a-sandbox)). We installed Node.js 24 from NodeSource in 13 s:

```bash
sbx exec $id --timeout 600 'curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - >/dev/null && sudo apt-get install -y nodejs >/dev/null && node --version'
```

```text
v24.22.1
```

### What goes wrong with the defaults

`astro dev --host 0.0.0.0 --port 4321`, hosted on 4321, answered `403` through the private URL:

```text
Blocked request. This host ("ample-vital-ember-4321.box.sandboxer.com") is not allowed.
To allow this host, add "ample-vital-ember-4321.box.sandboxer.com" to `server.allowedHosts` in vite.config.js.
```

The message mentions `vite.config.js`, but in an Astro project the setting goes in `astro.config.mjs` as [`server.allowedHosts`](https://docs.astro.build/en/reference/configuration-reference/#serverallowedhosts). There is also an `--allowed-hosts` flag on `astro dev`. A leading dot allows every subdomain, so the config keeps working on a fork, which gets a new subdomain.

### The fix

```javascript
// astro.config.mjs
// @ts-check
import { defineConfig } from "astro/config";

export default defineConfig({
  site: "https://ample-vital-ember-4321.box.sandboxer.com",
  server: {
    host: "0.0.0.0",
    port: 4321,
    allowedHosts: [".box.sandboxer.com"],
  },
});
```

With `host` and `port` in the config, the unit's command is just `npx astro dev`. We used the same unit as for Hugo, with `WorkingDirectory=/home/user/site` and `ExecStart=/usr/bin/npx astro dev`. The page then returned `200` in 0.28 s.

[`site`](https://docs.astro.build/en/reference/configuration-reference/#site) is what `Astro.site` returns, so canonical links and anything an integration builds from it use the hosted URL. With `<link rel="canonical" href={new URL(Astro.url.pathname, Astro.site)} />` in the page, the built `dist/index.html` contained:

```text
<link rel="canonical" href="https://ample-vital-ember-4321.box.sandboxer.com/">
```

Set `site` back to your real domain before a production build.

Hot module reload needed no change. The injected `/@vite/client` had `hmrPort = null`, so the browser connects back on the page's own host and port 443. We connected to the HMR socket through the private URL with the `vite-hmr` protocol, the cookie and the `wsToken` from `/@vite/client`, then changed the page's `<h1>`:

```text
pre-edit {"type":"connected"}
edit sent after 214 ms
+227ms {"type":"full-reload"}
```

## Build times

Both projects had about 300 pages of Markdown, roughly 3 KB each, built on the `small` sandbox.

| | Hugo v0.168.0 | Astro 7.4.2 |
| - | - | - |
| Pages | 338 (300 posts plus taxonomy pages) | 301 |
| Build | 150, 175, 152 ms | 4.22 s reported; 5.3, 4.6, 5.5 s wall clock |
| Scaffold and install | one `.deb`, no dependencies (not timed) | 26.3 s, plus 13 s for Node.js 24 |

These are themeless starters. Real themes, image processing and integrations add to both. To keep `node_modules` between projects, save a set-up sandbox with [`sbx snapshot`](/guides/cache-dependencies-in-a-named-snapshot) and start new ones from it.

## After stop, resume and fork

`sbx stop --wait` took 2.2 s and `sbx resume` 1.3 s with `memoryRestored: true`. Both dev servers were still running, and both URLs answered `200` with the cookie from before the stop. If a resume lands on a machine with a different CPU type, the sandbox boots from disk instead. The enabled systemd units then start the servers again, but the browser has to reconnect.

A fork gets a new subdomain and no hosted ports. Host the ports again on the fork. For Hugo, rewrite the unit, because `--baseURL` holds the old name. For Astro, update `site`. `allowedHosts` already covers the new name.

## FAQ

### Why does my Hugo page load but never reload on save?

The live reload script connects to the port in its `port=` parameter, 1313 by default, and the hosted URL only listens on 443. Start `hugo server` with `--liveReloadPort 443` and `--appendPort=false`.

### Why does Astro answer 403 "Blocked request"?

Vite, which Astro's dev server uses, rejects Host headers it doesn't know. Add `allowedHosts: [".box.sandboxer.com"]` under `server` in `astro.config.mjs`, or pass `--allowed-hosts`.

### Does live reload work on a private URL?

Yes. Opening the token link once sets a cookie, and the browser sends it on the WebSocket. We tested both Hugo's LiveReload socket and Astro's HMR socket that way.

### Which sandbox size should I use?

`small` was enough for both 300-page sites. Hugo used about 100 MB of RAM while serving. Use `default` for large Astro sites with heavy integrations or image processing.

### Do I need --localhost when I host the port?

No, not with these commands. Both servers were bound to `0.0.0.0`. If you leave Astro's `host` unset, it listens on localhost only. Then host the port with `--localhost`, or set `host`.

## Related

- [How do I give a port in a cloud sandbox an HTTPS URL?](/guides/give-a-sandbox-port-an-https-url)
- [How do I get a live preview URL for every pull request?](/guides/get-a-preview-url-for-every-pull-request)
- [What's pre-installed in a sandbox?](/guides/whats-preinstalled-in-a-sandbox)
- [Hosting](/docs/hosting)
