A small, iPad-friendly publisher for a static website hosted on a Linux server. It accepts a website ZIP, validates and safely extracts it, creates a timestamped release, and atomically switches nginx to the new version. The five most recent releases remain available for one-tap rollback.
- Export or download the website as a ZIP.
- Open the private publisher page on the iPad.
- Choose the ZIP and tap Publish website.
- The existing website remains live until the replacement is completely unpacked and validated.
The ZIP must contain index.html either at its root or inside one top-level folder.
/opt/sites/propper-purrs/
├── current -> releases/2026-08-26-193015
├── releases/
│ ├── 2026-08-26-190102/
│ └── 2026-08-26-193015/
└── uploads/
The nginx container mounts the whole propper-purrs directory read-only and serves /srv/site/current. Mounting the parent directory is important: it lets nginx see atomic symlink changes without restarting the container.
Clone the repository on the Linux host, then identify the Docker network used by Nginx Proxy Manager:
git clone https://github.com/bytemaster-0xff/siteuploader.git
cd siteuploader
docker network ls
cp .env.example .envEdit .env if the network is not named npm_default or if the site should live somewhere other than /opt/sites/propper-purrs.
Create the managed directory with the container user's ownership:
sudo install -d -o 1654 -g 1654 /opt/sites/propper-purrsIf the existing website is in a separate directory, import it before replacing the existing nginx container:
chmod +x scripts/bootstrap-existing-site.sh
sudo ./scripts/bootstrap-existing-site.sh /path/to/existing/site /opt/sites/propper-purrsThen start the two managed containers:
docker compose up -d --build
docker compose psIf propper-purrs-site already exists as a standalone container, remove that container immediately before the docker compose up command. Its files remain on the host; the bootstrap step above provides the managed copy and rollback point.
Create or update these proxy hosts:
| Public host | Forward hostname | Port | Access |
|---|---|---|---|
propperpurrs.com |
propper-purrs-site |
80 | Public |
publish.propperpurrs.com |
propper-purrs-publisher |
8080 | Private Access List |
For both hosts, enable SSL and Force SSL. On the publisher host, select an NPM Access List containing only the intended publisher account. Do not expose the publisher container directly with a host port.
NPM may default to a request-size limit smaller than 100 MB. Add this under the publisher proxy host's Advanced configuration:
client_max_body_size 100m;
proxy_request_buffering off;- Rejects ZIP traversal paths and symbolic links.
- Limits ZIP uploads to 100 MB, expanded content to 500 MB, and archive entries to 10,000.
- Requires a root
index.htmlbefore a release can go live. - Extracts into a private staging directory and switches releases atomically.
- Mounts website content read-only in nginx.
- Uses a same-origin request token in addition to the NPM Access List.
- Exposes no general-purpose file browser or shell access.
NPM is the authentication boundary. The publisher should remain reachable only through its password-protected proxy host.
ASP.NET Core environment variables use the Publisher__ prefix:
| Variable | Default |
|---|---|
Publisher__SiteRoot |
/sites/propper-purrs |
Publisher__MaxUploadBytes |
104857600 |
Publisher__MaxExtractedBytes |
524288000 |
Publisher__MaxArchiveEntries |
10000 |
Publisher__ReleasesToKeep |
5 |
To inspect releases and point current at a known-good version:
cd /opt/sites/propper-purrs
ls -1 releases
ln -sfn "releases/YYYY-MM-DD-HHMMSS" .current-next
mv -Tf .current-next currentThe second command is an atomic rename, matching the operation used by the application.
dotnet test tests/PropperPurrs.Publisher.Tests/PropperPurrs.Publisher.Tests.csproj
docker build -t propper-purrs-publisher:dev .