feat(files): gate files. browse behind FILES_BROWSE (on|off)
Per-host toggle for the read-only, no-auth browse of the shared upload dir,
so it can stay on for the LAN-party host (oxps) and be off on the
internet-facing one (cochi). Defaults to `on` — existing hosts unchanged.
Neither layer has conditionals, so each gets the value substituted in:
- caddy: docker-compose mounts 62-files.${FILES_BROWSE:-on}.caddy at the
fixed conf.d target (62-files.caddy -> 62-files.on.caddy, plus a
comments-only 62-files.off.caddy stub — caddy needs a parseable file
there). Same trick as FILES_MOUNT_MODE.
- nginx: render-nginx.sh splices nginx/files-browse.server.tmpl into
${FILES_BROWSE_SERVER} and adds/omits the name in the port-80 redirect.
This is the gate that matters; caddy is bound to 127.0.0.1.
Applying a change needs render-nginx.sh --install plus a caddy
--force-recreate (single-file bind mount pins the inode). With `off`,
drop `files` from LE_SUBDOMAINS on that host. upload. is unaffected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
92c6f5fc8f
commit
8507ffd020
9 files changed
+122
-33
No files matched your search
@@ -64,6 +64,17 @@ FILES_DATA_DIR=/srv/ulicraft-files
|
|||||||
# value is substituted straight into the volume string.
|
# value is substituted straight into the volume string.
|
||||||
FILES_MOUNT_MODE=rw
|
FILES_MOUNT_MODE=rw
|
||||||
|
|
||||||
|
# files.${BASE_DOMAIN} — the read-only, NO-AUTH browse of the same share.
|
||||||
|
# Per-host toggle, `on` | `off` (default on). It gates two layers, both reading
|
||||||
|
# this var: the caddy vhost (docker-compose.yml substitutes the value into the
|
||||||
|
# conf.d mount path — 62-files.on.caddy vs the 62-files.off.caddy stub) and the
|
||||||
|
# public nginx vhost (tooling/render-nginx.sh). `off` also drops the name from
|
||||||
|
# the port-80 redirect, so no cert is needed — drop `files` from LE_SUBDOMAINS
|
||||||
|
# too on that host. upload. (filestash, authenticated) is NOT affected.
|
||||||
|
# After changing it: tooling/render-nginx.sh --install
|
||||||
|
# docker compose up -d --force-recreate caddy
|
||||||
|
FILES_BROWSE=on
|
||||||
|
|
||||||
# Runtime uid:gid of the filestash process (docker `user:`). Leave at the image
|
# Runtime uid:gid of the filestash process (docker `user:`). Leave at the image
|
||||||
# default 1000:1000 UNLESS you want files it writes into the share owned by a
|
# default 1000:1000 UNLESS you want files it writes into the share owned by a
|
||||||
# host account. To own the share as `oier` on oxps: set both to 1002 and run
|
# host account. To own the share as `oier` on oxps: set both to 1002 and run
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ Internet ── host nginx (TLS, LE certs, HSTS) ──► caddy (127.0.0.1:8880
|
|||||||
avatar.${BASE_DOMAIN} ─► nmsr (skin/avatar renderer, Drasl-backed)
|
avatar.${BASE_DOMAIN} ─► nmsr (skin/avatar renderer, Drasl-backed)
|
||||||
status.${BASE_DOMAIN} ─► uptime-kuma (status page + monitors)
|
status.${BASE_DOMAIN} ─► uptime-kuma (status page + monitors)
|
||||||
upload.${BASE_DOMAIN} ─► filestash (player file share, ONE shared login, writable)
|
upload.${BASE_DOMAIN} ─► filestash (player file share, ONE shared login, writable)
|
||||||
files.${BASE_DOMAIN} ─► caddy (read-only, no-auth browse of the same share dir)
|
files.${BASE_DOMAIN} ─► caddy (read-only, no-auth browse of the same share dir;
|
||||||
|
per-host on/off via FILES_BROWSE — off on cochi)
|
||||||
distribution.${BASE} ─► static site from ANOTHER repo (DISTRIBUTION_WEB_ROOT)
|
distribution.${BASE} ─► static site from ANOTHER repo (DISTRIBUTION_WEB_ROOT)
|
||||||
|
|
||||||
minecraft :25565 (+ 24454/udp Simple Voice Chat)
|
minecraft :25565 (+ 24454/udp Simple Voice Chat)
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Files browse DISABLED (FILES_BROWSE=off in .env).
|
||||||
|
#
|
||||||
|
# This stub is mounted at /etc/caddy/conf.d/62-files.caddy instead of
|
||||||
|
# 62-files.on.caddy, so caddy defines NO vhost for files.${BASE_DOMAIN} and the
|
||||||
|
# share is not served read-only anywhere. Uploads via upload.${BASE_DOMAIN}
|
||||||
|
# (filestash) are unaffected — that is a separate service.
|
||||||
|
#
|
||||||
|
# Caddy needs a parseable file here, not an absent one: the Caddyfile imports
|
||||||
|
# conf.d/*.caddy and the mount target is fixed. Comments only, on purpose.
|
||||||
File renamed without changes.
+8
-1
@@ -172,7 +172,14 @@ services:
|
|||||||
- ./caddy/conf.d/40-avatar.caddy:/etc/caddy/conf.d/40-avatar.caddy:ro
|
- ./caddy/conf.d/40-avatar.caddy:/etc/caddy/conf.d/40-avatar.caddy:ro
|
||||||
- ./caddy/conf.d/50-status.caddy:/etc/caddy/conf.d/50-status.caddy:ro
|
- ./caddy/conf.d/50-status.caddy:/etc/caddy/conf.d/50-status.caddy:ro
|
||||||
- ./caddy/conf.d/60-upload.caddy:/etc/caddy/conf.d/60-upload.caddy:ro
|
- ./caddy/conf.d/60-upload.caddy:/etc/caddy/conf.d/60-upload.caddy:ro
|
||||||
- ./caddy/conf.d/62-files.caddy:/etc/caddy/conf.d/62-files.caddy:ro
|
# files.${BASE_DOMAIN} read-only browse, gated by FILES_BROWSE (on|off).
|
||||||
|
# The value is substituted straight into the source path — `on` mounts the
|
||||||
|
# real vhost, `off` a comment-only stub — because compose has no
|
||||||
|
# conditionals (same trick as FILES_MOUNT_MODE). Changing it needs
|
||||||
|
# `docker compose up -d --force-recreate caddy`: a single-file bind mount
|
||||||
|
# pins the inode. The public gate is the nginx vhost (render-nginx.sh
|
||||||
|
# reads the same var); this one keeps caddy itself from serving the share.
|
||||||
|
- ./caddy/conf.d/62-files.${FILES_BROWSE:-on}.caddy:/etc/caddy/conf.d/62-files.caddy:ro
|
||||||
- ./caddy/conf.d/70-generators.caddy:/etc/caddy/conf.d/70-generators.caddy:ro
|
- ./caddy/conf.d/70-generators.caddy:/etc/caddy/conf.d/70-generators.caddy:ro
|
||||||
- ./www:/srv/www:ro
|
- ./www:/srv/www:ro
|
||||||
- ./launcher:/srv/launcher:ro
|
- ./launcher:/srv/launcher:ro
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Read-only static browse of the same share (files.) — caddy serves the dir
|
||||||
|
# listing, no auth. Read-only, so no upload tuning: standard 4m body is fine.
|
||||||
|
#
|
||||||
|
# Rendered into ulicraft-caddy.conf.tmpl at ${FILES_BROWSE_SERVER} by
|
||||||
|
# render-nginx.sh, ONLY when FILES_BROWSE=on. With `off` the placeholder
|
||||||
|
# collapses to a comment, so the name has no TLS vhost at all (and needs no
|
||||||
|
# cert — drop `files` from LE_SUBDOMAINS on that host).
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
listen [::]:443 ssl http2;
|
||||||
|
server_name files.${BASE_DOMAIN};
|
||||||
|
|
||||||
|
ssl_certificate ${APP_DIR}/certs/files.${BASE_DOMAIN}/cert.pem;
|
||||||
|
ssl_certificate_key ${APP_DIR}/certs/files.${BASE_DOMAIN}/key.pem;
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://ulicraft_caddy;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ limit_req_zone $binary_remote_addr zone=files_admin:10m rate=6r/m;
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
listen [::]:80;
|
listen [::]:80;
|
||||||
server_name ${BASE_DOMAIN} www.${BASE_DOMAIN} auth.${BASE_DOMAIN} distribution.${BASE_DOMAIN} avatar.${BASE_DOMAIN} status.${BASE_DOMAIN} upload.${BASE_DOMAIN} files.${BASE_DOMAIN} generators.${BASE_DOMAIN};
|
server_name ${BASE_DOMAIN} www.${BASE_DOMAIN} auth.${BASE_DOMAIN} distribution.${BASE_DOMAIN} avatar.${BASE_DOMAIN} status.${BASE_DOMAIN} upload.${BASE_DOMAIN}${FILES_BROWSE_NAME} generators.${BASE_DOMAIN};
|
||||||
return 301 https://$host$request_uri;
|
return 301 https://$host$request_uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,25 +191,10 @@ server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Read-only static browse of the same share (files.) — caddy serves the dir
|
# files. (read-only browse) goes here — the whole server block, or a single
|
||||||
# listing, no auth. Read-only, so no upload tuning: standard 4m body is fine.
|
# comment line when FILES_BROWSE=off. Source: nginx/files-browse.server.tmpl,
|
||||||
server {
|
# spliced in by render-nginx.sh.
|
||||||
listen 443 ssl http2;
|
${FILES_BROWSE_SERVER}
|
||||||
listen [::]:443 ssl http2;
|
|
||||||
server_name files.${BASE_DOMAIN};
|
|
||||||
|
|
||||||
ssl_certificate ${APP_DIR}/certs/files.${BASE_DOMAIN}/cert.pem;
|
|
||||||
ssl_certificate_key ${APP_DIR}/certs/files.${BASE_DOMAIN}/key.pem;
|
|
||||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://ulicraft_caddy;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 443 ssl http2;
|
listen 443 ssl http2;
|
||||||
|
|||||||
+35
-9
@@ -7,9 +7,9 @@
|
|||||||
>
|
>
|
||||||
> The same `${FILES_DATA_DIR}` is also exposed at **`files.${BASE_DOMAIN}`** as a
|
> The same `${FILES_DATA_DIR}` is also exposed at **`files.${BASE_DOMAIN}`** as a
|
||||||
> plain caddy `file_server browse` — **read-only, no auth**, every uploaded file
|
> plain caddy `file_server browse` — **read-only, no auth**, every uploaded file
|
||||||
> publicly listable (`62-files.caddy`). Filestash gets its own subdomain because
|
> publicly listable (`62-files.on.caddy`, toggled per host by `FILES_BROWSE`).
|
||||||
> it cannot be served under a subpath (SPA hardcodes `/api`, `/assets`, `/login`
|
> Filestash gets its own subdomain because it cannot be served under a subpath
|
||||||
> at root).
|
> (SPA hardcodes `/api`, `/assets`, `/login` at root).
|
||||||
>
|
>
|
||||||
> **Not** a backup browser, **not** a mod mirror. See "What goes in it".
|
> **Not** a backup browser, **not** a mod mirror. See "What goes in it".
|
||||||
>
|
>
|
||||||
@@ -30,7 +30,7 @@ Internet ── host nginx (TLS, LE cert files.) ──► caddy file_server (ro
|
|||||||
| Thing | Value |
|
| Thing | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Vhost (filestash) | `upload.${BASE_DOMAIN}` |
|
| Vhost (filestash) | `upload.${BASE_DOMAIN}` |
|
||||||
| Vhost (static browse) | `files.${BASE_DOMAIN}` — read-only, no auth, `62-files.caddy` |
|
| Vhost (static browse) | `files.${BASE_DOMAIN}` — read-only, no auth, `62-files.on.caddy`, per-host `FILES_BROWSE=on\|off` |
|
||||||
| Image | `machines/filestash` **pinned by digest** (upstream ships only `latest`) |
|
| Image | `machines/filestash` **pinned by digest** (upstream ships only `latest`) |
|
||||||
| Container port | `8334`, runs as **uid/gid 1000** (`filestash`) |
|
| Container port | `8334`, runs as **uid/gid 1000** (`filestash`) |
|
||||||
| Auth | htpasswd, **one shared account**, `FILES_USER` / `FILES_PASSWORD_HASH` |
|
| Auth | htpasswd, **one shared account**, `FILES_USER` / `FILES_PASSWORD_HASH` |
|
||||||
@@ -64,13 +64,38 @@ blocklisting (which would take `auth.` and the apex down with it, breaking login
|
|||||||
and the launcher for every player) all land on you. `render-config.sh` prints a
|
and the launcher for every player) all land on you. `render-config.sh` prints a
|
||||||
loud warning when it renders this mode.
|
loud warning when it renders this mode.
|
||||||
|
|
||||||
**`files.` is unconditionally public read.** Independent of `FILES_AUTH`, the
|
**`files.` is public read wherever it is enabled.** Independent of `FILES_AUTH`,
|
||||||
`62-files.caddy` static browse serves the whole share with **no auth** — anyone
|
the static browse serves the whole share with **no auth** — anyone who reaches
|
||||||
who knows `files.${BASE_DOMAIN}` can list and download every uploaded file. This
|
`files.${BASE_DOMAIN}` can list and download every uploaded file. This is
|
||||||
is deliberate (chosen 2026-07-24). The htpasswd login only gates *writes* (via
|
deliberate (chosen 2026-07-24). The htpasswd login only gates *writes* (via
|
||||||
`upload.`); it never gated reads at this URL. Don't put anything private in the
|
`upload.`); it never gated reads at this URL. Don't put anything private in the
|
||||||
share.
|
share.
|
||||||
|
|
||||||
|
**`FILES_BROWSE=on|off` turns the whole `files.` vhost off per host** (added
|
||||||
|
2026-07-30) — the answer to "I want it on `oxps` (LAN party) but not on `cochi`
|
||||||
|
(internet-facing)". One var, two layers, because each has its own conditional
|
||||||
|
problem:
|
||||||
|
|
||||||
|
| Layer | Mechanism |
|
||||||
|
|---|---|
|
||||||
|
| caddy | `docker-compose.yml` substitutes the value into the mount source: `./caddy/conf.d/62-files.${FILES_BROWSE:-on}.caddy` → `/etc/caddy/conf.d/62-files.caddy`. `62-files.off.caddy` is a comments-only stub (compose has no conditionals, and caddy needs a parseable file at the fixed mount target — same trick as `FILES_MOUNT_MODE`). |
|
||||||
|
| nginx | `render-nginx.sh` splices `nginx/files-browse.server.tmpl` into `${FILES_BROWSE_SERVER}` when `on`, or a comment when `off`, and adds/omits the name in the port-80 redirect (nginx has no conditionals either). |
|
||||||
|
|
||||||
|
nginx is the gate that matters — with no TLS vhost the name is unreachable from
|
||||||
|
the internet regardless of caddy (caddy is bound to `127.0.0.1`). The caddy stub
|
||||||
|
is the second layer, and it also matters on hosts reached **directly** on caddy's
|
||||||
|
port rather than through nginx (`oxps` on the LAN). Applying a change:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
tooling/render-nginx.sh --install # nginx vhost in/out + reload
|
||||||
|
docker compose up -d --force-recreate caddy # single-file bind mount pins the inode
|
||||||
|
```
|
||||||
|
|
||||||
|
`restart caddy` is NOT enough (the conf mount is a single file — see
|
||||||
|
`plan/14-deploy.md`). With `off`, `files` also needs dropping from
|
||||||
|
`LE_SUBDOMAINS` on that host: no vhost, no cert to renew. `upload.` (filestash,
|
||||||
|
authenticated, writable) is untouched by this var.
|
||||||
|
|
||||||
**Per-host auth status (the two prod envs diverge):**
|
**Per-host auth status (the two prod envs diverge):**
|
||||||
|
|
||||||
| Host | `upload.` reachability | `FILES_AUTH` | Notes |
|
| Host | `upload.` reachability | `FILES_AUTH` | Notes |
|
||||||
@@ -106,13 +131,14 @@ See `.env.example` for the annotated block. Summary:
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `FILES_DATA_DIR` | host path of the share, outside the repo |
|
| `FILES_DATA_DIR` | host path of the share, outside the repo |
|
||||||
| `FILES_MOUNT_MODE` | literal mount flag, `rw` \| `ro` |
|
| `FILES_MOUNT_MODE` | literal mount flag, `rw` \| `ro` |
|
||||||
|
| `FILES_BROWSE` | `on` \| `off` — serve the `files.` read-only browse on this host |
|
||||||
| `FILES_AUTH` | `htpasswd` \| `passthrough` \| `none` |
|
| `FILES_AUTH` | `htpasswd` \| `passthrough` \| `none` |
|
||||||
| `FILES_USER` | shared login name |
|
| `FILES_USER` | shared login name |
|
||||||
| `FILES_PASSWORD_HASH` | **`$6$`**, from `openssl passwd -6 '<pw>'` |
|
| `FILES_PASSWORD_HASH` | **`$6$`**, from `openssl passwd -6 '<pw>'` |
|
||||||
| `FILES_ADMIN_PASSWORD_HASH` | bcrypt, from `docker run --rm caddy:alpine caddy hash-password --plaintext '<pw>'` |
|
| `FILES_ADMIN_PASSWORD_HASH` | bcrypt, from `docker run --rm caddy:alpine caddy hash-password --plaintext '<pw>'` |
|
||||||
| `FILES_SECRET_KEY` | `openssl rand -hex 16`, must be non-empty |
|
| `FILES_SECRET_KEY` | `openssl rand -hex 16`, must be non-empty |
|
||||||
| `FILES_LOCAL_SECRET` | `openssl rand -hex 24`, unlocks the local backend |
|
| `FILES_LOCAL_SECRET` | `openssl rand -hex 24`, unlocks the local backend |
|
||||||
| `LE_SUBDOMAINS` | must include `upload` **and** `files` (both are vhosts) |
|
| `LE_SUBDOMAINS` | must include `upload`, plus `files` when `FILES_BROWSE=on` |
|
||||||
|
|
||||||
`filestash/config.json` is **rendered and gitignored** — it holds every hash and
|
`filestash/config.json` is **rendered and gitignored** — it holds every hash and
|
||||||
both secrets. Only the `.tmpl` is tracked.
|
both secrets. Only the `.tmpl` is tracked.
|
||||||
|
|||||||
+28
-2
@@ -17,6 +17,11 @@
|
|||||||
# APP_DIR host path to this repo (cert files live under APP_DIR/certs)
|
# APP_DIR host path to this repo (cert files live under APP_DIR/certs)
|
||||||
# — defaults to the repo root (this checkout)
|
# — defaults to the repo root (this checkout)
|
||||||
# SITE_NAME installed filename (default ulicraft.conf)
|
# SITE_NAME installed filename (default ulicraft.conf)
|
||||||
|
# FILES_BROWSE on|off (default on) — emit the files.${BASE_DOMAIN}
|
||||||
|
# read-only browse vhost. `off` omits the server block AND
|
||||||
|
# the name from the port-80 redirect, so the host serves
|
||||||
|
# nothing for it (no cert needed either). Same var gates the
|
||||||
|
# caddy side in docker-compose.yml.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
cd "$(dirname "$0")/.." # repo root
|
cd "$(dirname "$0")/.." # repo root
|
||||||
@@ -40,7 +45,28 @@ SITE_NAME="${SITE_NAME:-ulicraft.conf}"
|
|||||||
export BASE_DOMAIN APP_DIR
|
export BASE_DOMAIN APP_DIR
|
||||||
export CADDY_HTTP_PORT="${CADDY_HTTP_PORT:-80}"
|
export CADDY_HTTP_PORT="${CADDY_HTTP_PORT:-80}"
|
||||||
|
|
||||||
rendered="$(envsubst '$BASE_DOMAIN $APP_DIR $CADDY_HTTP_PORT' < "$TEMPLATE")"
|
# files.${BASE_DOMAIN} — the no-auth read-only browse. Per-host toggle: on for
|
||||||
|
# the LAN-party box, off where the share must not be publicly listable. nginx
|
||||||
|
# has no conditionals, so the vhost is a separate template spliced in here (or
|
||||||
|
# replaced by a comment). Keep in sync with the caddy mount in docker-compose.yml,
|
||||||
|
# which reads the same var.
|
||||||
|
: "${FILES_BROWSE:=on}"
|
||||||
|
FILES_BROWSE_TEMPLATE="${FILES_BROWSE_TEMPLATE:-nginx/files-browse.server.tmpl}"
|
||||||
|
case "$FILES_BROWSE" in
|
||||||
|
on)
|
||||||
|
[ -f "$FILES_BROWSE_TEMPLATE" ] || die "template not found: $FILES_BROWSE_TEMPLATE"
|
||||||
|
FILES_BROWSE_NAME=" files.${BASE_DOMAIN}"
|
||||||
|
FILES_BROWSE_SERVER="$(envsubst '$BASE_DOMAIN $APP_DIR' < "$FILES_BROWSE_TEMPLATE")"
|
||||||
|
;;
|
||||||
|
off)
|
||||||
|
FILES_BROWSE_NAME=""
|
||||||
|
FILES_BROWSE_SERVER="# files.${BASE_DOMAIN} disabled on this host (FILES_BROWSE=off in .env)."
|
||||||
|
;;
|
||||||
|
*) die "FILES_BROWSE must be 'on' or 'off' (got '$FILES_BROWSE')" ;;
|
||||||
|
esac
|
||||||
|
export FILES_BROWSE_NAME FILES_BROWSE_SERVER
|
||||||
|
|
||||||
|
rendered="$(envsubst '$BASE_DOMAIN $APP_DIR $CADDY_HTTP_PORT $FILES_BROWSE_NAME $FILES_BROWSE_SERVER' < "$TEMPLATE")"
|
||||||
|
|
||||||
if [ "${1:-}" != "--install" ]; then
|
if [ "${1:-}" != "--install" ]; then
|
||||||
printf '%s\n' "$rendered"
|
printf '%s\n' "$rendered"
|
||||||
@@ -52,7 +78,7 @@ command -v nginx >/dev/null 2>&1 || die "nginx not found on PATH"
|
|||||||
avail="/etc/nginx/sites-available/$SITE_NAME"
|
avail="/etc/nginx/sites-available/$SITE_NAME"
|
||||||
enabled="/etc/nginx/sites-enabled/$SITE_NAME"
|
enabled="/etc/nginx/sites-enabled/$SITE_NAME"
|
||||||
|
|
||||||
log "installing $TEMPLATE -> $avail (APP_DIR=$APP_DIR, CADDY_HTTP_PORT=$CADDY_HTTP_PORT)"
|
log "installing $TEMPLATE -> $avail (APP_DIR=$APP_DIR, CADDY_HTTP_PORT=$CADDY_HTTP_PORT, FILES_BROWSE=$FILES_BROWSE)"
|
||||||
printf '%s\n' "$rendered" | sudo tee "$avail" >/dev/null
|
printf '%s\n' "$rendered" | sudo tee "$avail" >/dev/null
|
||||||
sudo ln -sf "$avail" "$enabled"
|
sudo ln -sf "$avail" "$enabled"
|
||||||
sudo nginx -t
|
sudo nginx -t
|
||||||
|
|||||||
Reference in new issue
Block a user