From 12ac0b48418d9e693f603fd22aeed44d9a9dde88 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 03:23:06 +0000 Subject: [PATCH 1/3] docs: make self-hosting turnkey on Synology/VPS - docker-compose.yml: use prebuilt ghcr.io/largerio/secret image instead of build:. (commented build kept for contributors); load config via env_file - .env.example: neutral APP_URL default + clarify exposed vs internal port - docs/self-hosting.md: new guide (VPS no-clone deploy, Synology Container Manager, reverse proxy, backup/restore) - README: two-path Quick Start + link to the self-hosting guide https://claude.ai/code/session_01WkTWZvbmaA6aUT5hQSTz9m --- .env.example | 7 ++- README.md | 37 ++++++++++-- docker-compose.yml | 24 +++----- docs/self-hosting.md | 139 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 185 insertions(+), 22 deletions(-) create mode 100644 docs/self-hosting.md diff --git a/.env.example b/.env.example index 9f9ca48..bc2e5bb 100644 --- a/.env.example +++ b/.env.example @@ -6,7 +6,10 @@ SERVER_ENCRYPTION_KEY= # === Application === APP_NAME=Secret -APP_URL=https://secret.larger.io +# Public URL where your instance is reachable. Set this to your own domain in +# production (e.g. https://secret.example.com) — it is used for CORS, the +# OpenAPI spec, robots.txt and sitemap.xml. +APP_URL=http://localhost:3000 APP_DESCRIPTION=Secure zero-knowledge note and file sharing # === Design / Branding === @@ -24,6 +27,8 @@ DEFAULT_EXPIRY=86400 MAX_EXPIRY=604800 # === Server === +# Exposed (public) port for the web app. The API runs internally on 3001 and is +# proxied by the web server — do not confuse the two; you only expose PORT. PORT=3000 HOST=0.0.0.0 diff --git a/README.md b/README.md index 6a09492..3e60825 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ A modern, open-source alternative to PrivateBin, OneTimeSecret, and Yopass — b - [How It Works](#how-it-works) - [Configuration](#configuration) - [S3 Storage (optional)](#s3-storage-optional) +- [Self-Hosting (Synology, VPS, backups)](#self-hosting) - [Updating](#updating) - [Reverse Proxy](#reverse-proxy) - [Development](#development) @@ -63,21 +64,37 @@ A modern, open-source alternative to PrivateBin, OneTimeSecret, and Yopass — b ## Quick Start +**Fastest — prebuilt image (no clone, no build):** + +```bash +mkdir secret && cd secret +curl -O https://raw.githubusercontent.com/largerio/secret/main/docker-compose.yml +curl -o .env https://raw.githubusercontent.com/largerio/secret/main/.env.example + +# Generate a server encryption key (REQUIRED) and paste it into .env +openssl rand -base64 32 # → SERVER_ENCRYPTION_KEY= +# Set APP_URL to your public URL (or leave http://localhost:3000 for local) + +docker compose up -d # pulls ghcr.io/largerio/secret:latest +``` + +**From source (for contributors):** + ```bash git clone https://github.com/largerio/secret.git cd secret cp .env.example .env +openssl rand -base64 32 # → set SERVER_ENCRYPTION_KEY in .env -# Generate a server encryption key (REQUIRED) — pick one -openssl rand -base64 32 -node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" -# Paste the output into .env as SERVER_ENCRYPTION_KEY - +# Uncomment `build: .` (and comment out `image:`) in docker-compose.yml to build locally docker compose up -d ``` Open `http://localhost:3000`. API documentation is available at `/api/v1/docs` ([Scalar](https://scalar.com/)). +For Synology NAS, VPS, reverse proxy and backup instructions, see the +[Self-Hosting guide](docs/self-hosting.md). + ## SDK Use the JavaScript/TypeScript SDK to interact with any Secret instance programmatically: @@ -157,6 +174,16 @@ MAX_FILE_SIZE=104857600 # 100 MB Compatible with AWS S3, MinIO, and Cloudflare R2. +## Self-Hosting + +Step-by-step deployment guides for common setups are in +**[docs/self-hosting.md](docs/self-hosting.md)**: + +- **VPS / any Docker host** — two-file deploy (`docker-compose.yml` + `.env`), no clone +- **Synology NAS** — DSM Container Manager walkthrough, with volume/permission notes +- **Reverse proxy & HTTPS** — including DSM's built-in reverse proxy +- **Backup & restore** — snapshotting the `secret-data` volume safely + ## Updating ```bash diff --git a/docker-compose.yml b/docker-compose.yml index db380a6..ec60218 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,28 +1,20 @@ services: app: - build: . + # Pulls the prebuilt image published by CI — no clone or local build needed. + # To build from source instead, comment out `image:` and uncomment `build: .`. + image: ghcr.io/largerio/secret:latest + # build: . ports: - "${PORT:-3000}:3000" volumes: - secret-data:/app/data + # All configuration is read from the .env file (copy .env.example → .env). + env_file: + - .env environment: + # Force data paths into the persistent volume regardless of .env contents. - DATABASE_PATH=/app/data/secret.db - FILES_PATH=/app/data/files - - SERVER_ENCRYPTION_KEY=${SERVER_ENCRYPTION_KEY} - - APP_URL=${APP_URL:-http://localhost:3000} - - APP_NAME=${APP_NAME:-Secret} - - MAX_FILE_SIZE=${MAX_FILE_SIZE:-10485760} - - MAX_FILES_PER_NOTE=${MAX_FILES_PER_NOTE:-10} - - DEFAULT_EXPIRY=${DEFAULT_EXPIRY:-86400} - - MAX_EXPIRY=${MAX_EXPIRY:-604800} - - BODY_SIZE_LIMIT=${BODY_SIZE_LIMIT:-105906176} - - STORAGE_BACKEND=${STORAGE_BACKEND:-local} - - S3_BUCKET=${S3_BUCKET:-} - - S3_REGION=${S3_REGION:-us-east-1} - - S3_ENDPOINT=${S3_ENDPOINT:-} - - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID:-} - - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY:-} - - S3_FORCE_PATH_STYLE=${S3_FORCE_PATH_STYLE:-false} restart: unless-stopped init: true read_only: true diff --git a/docs/self-hosting.md b/docs/self-hosting.md new file mode 100644 index 0000000..865f13c --- /dev/null +++ b/docs/self-hosting.md @@ -0,0 +1,139 @@ +# Self-Hosting Guide + +Deploy your own Secret instance in minutes — no clone, no build. The official +image is published to [`ghcr.io/largerio/secret`](https://ghcr.io/largerio/secret) +and runs as a single container. + +- [Quick deploy (VPS / any Docker host)](#quick-deploy-vps--any-docker-host) +- [Synology NAS (DSM 7 / Container Manager)](#synology-nas-dsm-7--container-manager) +- [Reverse proxy & HTTPS](#reverse-proxy--https) +- [Backup & restore](#backup--restore) +- [Updating](#updating) + +--- + +## Quick deploy (VPS / any Docker host) + +You only need two files — `docker-compose.yml` and `.env`. No git clone required. + +```bash +mkdir secret && cd secret + +# Grab the compose file and an env template +curl -O https://raw.githubusercontent.com/largerio/secret/main/docker-compose.yml +curl -o .env https://raw.githubusercontent.com/largerio/secret/main/.env.example + +# Generate the REQUIRED server encryption key and paste it into .env +openssl rand -base64 32 +# → set SERVER_ENCRYPTION_KEY= in .env + +# Set your public URL in .env, e.g. +# APP_URL=https://secret.example.com + +docker compose up -d +``` + +The container pulls `ghcr.io/largerio/secret:latest`, creates a persistent +`secret-data` volume, and serves the app on port `3000`. +Open `http://:3000` and you're live. + +> ⚠️ **Never change `SERVER_ENCRYPTION_KEY` after the first launch** — all +> existing notes become permanently unreadable. Back it up somewhere safe. + +--- + +## Synology NAS (DSM 7 / Container Manager) + +1. **Install Container Manager** from the DSM Package Center (if not already). +2. **Prepare a folder.** In File Station, create `docker/secret`. Put two files + in it: + - `docker-compose.yml` (from the link above) + - `.env` — start from `.env.example`, then set `SERVER_ENCRYPTION_KEY` + (`openssl rand -base64 32`) and `APP_URL` (e.g. `https://secret.example.com`). +3. **Create the project.** Container Manager → **Project** → **Create** → + set the path to `docker/secret` and import the existing `docker-compose.yml`. +4. **Run it.** Build/Start the project — Container Manager pulls + `ghcr.io/largerio/secret:latest` automatically. +5. **Access it.** Port `3000` is mapped to the NAS; browse to `http://:3000`. + +### Synology notes & caveats + +- The container runs as a **non-root user (uid 1001)** with a **read-only root + filesystem**; only the `secret-data` volume and an in-memory `/tmp` are + writable. This works out of the box on DSM with the named Docker volume — no + permission tweaks needed. +- **Prefer the named volume `secret-data`** (the default). If you instead + bind-mount a NAS folder (e.g. `/volume1/docker/secret/data:/app/data`), you + must give uid 1001 write access first: + ```bash + sudo chown -R 1001:1001 /volume1/docker/secret/data + ``` +- To change the exposed port (e.g. if 3000 is taken), set `PORT=8080` in `.env`. + +--- + +## Reverse proxy & HTTPS + +Put a reverse proxy in front to terminate TLS. Caddy and Nginx examples are in +the [main README](../README.md#reverse-proxy). Two reminders: + +- Set `client_max_body_size` (Nginx) / request body limits to at least + `MAX_CHUNKED_FILE_SIZE` (default 500 MB) so large uploads aren't rejected. +- Update `APP_URL` in `.env` to your `https://` domain. + +**On Synology** you can skip an external proxy and use the built-in one: +DSM → **Login Portal → Advanced → Reverse Proxy** → create a rule from +`secret.example.com:443` → `localhost:3000`. DSM manages the certificate. + +--- + +## Backup & restore + +Everything lives in the `secret-data` Docker volume: the SQLite database +(`secret.db` + WAL files) and the encrypted `files/` directory. + +### Back up + +```bash +# Stop the container first so the API checkpoints the WAL on SIGTERM +docker compose stop + +# Snapshot the volume into a tarball +docker run --rm \ + -v secret-data:/data \ + -v "$PWD":/backup \ + alpine tar czf /backup/secret-backup.tgz -C /data . + +docker compose start +``` + +Store `secret-backup.tgz` **together with your `SERVER_ENCRYPTION_KEY`** — the +data is useless without the key. + +### Restore + +```bash +# Recreate the volume from the tarball, then start fresh +docker volume create secret-data +docker run --rm \ + -v secret-data:/data \ + -v "$PWD":/backup \ + alpine sh -c "cd /data && tar xzf /backup/secret-backup.tgz" + +docker compose up -d +``` + +Make sure the restored instance uses the **same** `SERVER_ENCRYPTION_KEY` as the +one that produced the backup, or the notes won't decrypt. + +--- + +## Updating + +```bash +docker compose pull # fetch the latest image +docker compose up -d # recreate the container +docker image prune -f # clean up old layers +``` + +Your data lives in the volume, so updates never delete notes. From 9ed13d36f539cea033f5abbe969c49cafe51f588 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 11:52:47 +0000 Subject: [PATCH 2/3] ci: publish :latest + multi-arch image; add one-click deploy recipes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The compose/docs reference ghcr.io/largerio/secret:latest, but CI only tagged latest on semver releases (none exist) and built amd64 only — so the image was unpullable and ARM hosts unsupported. Fix both, then document turnkey deploys. - deploy.yml: tag latest on default-branch pushes; build linux/amd64+arm64 (adds setup-qemu-action for cross-build) - docs/self-hosting.md: one-liner docker run, Coolify, Portainer, Railway/Render recipes + multi-arch note - render.yaml: Render blueprint for one-click deploy https://claude.ai/code/session_01WkTWZvbmaA6aUT5hQSTz9m --- .github/workflows/deploy.yml | 5 ++++ docs/self-hosting.md | 57 ++++++++++++++++++++++++++++++++++++ render.yaml | 22 ++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 render.yaml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b41f236..528c75c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -128,6 +128,9 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + # QEMU enables cross-building the linux/arm64 image on the amd64 runner. + - uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 + - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 @@ -146,11 +149,13 @@ jobs: type=ref,event=branch type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} - id: build uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7 with: context: . + platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/docs/self-hosting.md b/docs/self-hosting.md index 865f13c..3efc377 100644 --- a/docs/self-hosting.md +++ b/docs/self-hosting.md @@ -5,11 +5,16 @@ image is published to [`ghcr.io/largerio/secret`](https://ghcr.io/largerio/secre and runs as a single container. - [Quick deploy (VPS / any Docker host)](#quick-deploy-vps--any-docker-host) +- [One-click & platform deploys](#one-click--platform-deploys) - [Synology NAS (DSM 7 / Container Manager)](#synology-nas-dsm-7--container-manager) - [Reverse proxy & HTTPS](#reverse-proxy--https) - [Backup & restore](#backup--restore) - [Updating](#updating) +> **Image architecture:** the official image is multi-arch (`linux/amd64` and +> `linux/arm64`), so it runs natively on x86 servers as well as ARM hosts — +> Synology ARM models, Raspberry Pi (64-bit), and Apple Silicon. + --- ## Quick deploy (VPS / any Docker host) @@ -42,6 +47,58 @@ Open `http://:3000` and you're live. --- +## One-click & platform deploys + +### One-liner `docker run` + +The fastest way to spin up a test instance — no files at all: + +```bash +# Generate and SAVE the key (without it, notes are unreadable forever) +KEY=$(openssl rand -base64 32); echo "SERVER_ENCRYPTION_KEY=$KEY" + +docker run -d --name secret -p 3000:3000 \ + -v secret-data:/app/data \ + -e SERVER_ENCRYPTION_KEY="$KEY" \ + -e APP_URL=http://localhost:3000 \ + ghcr.io/largerio/secret:latest +``` + +For a real deployment, set `APP_URL` to your public `https://` domain and put a +reverse proxy in front (see below). + +### Coolify + +Coolify gives you automatic HTTPS through its built-in proxy. + +1. **New Resource** → **Docker Image** → `ghcr.io/largerio/secret:latest` + (or **Docker Compose** and paste this repo's `docker-compose.yml`). +2. **Environment variables:** set `SERVER_ENCRYPTION_KEY` (generate one) and + `APP_URL` to the domain Coolify assigns. +3. **Persistent Storage:** add a volume mounted at **`/app/data`**. +4. **Ports:** expose `3000`. Coolify provisions the TLS certificate automatically. + +### Portainer + +1. **Stacks** → **Add stack** → **Web editor**. +2. Paste this repo's `docker-compose.yml`. +3. Fill in the environment variables (at least `SERVER_ENCRYPTION_KEY` and + `APP_URL`) in the editor, then **Deploy the stack**. + +### Railway / Render (PaaS) + +Deploy directly from the image `ghcr.io/largerio/secret:latest`. A +[`render.yaml`](../render.yaml) blueprint is included for one-click Render deploys. + +- ⚠️ **Attach a persistent disk/volume mounted at `/app/data`** — PaaS + filesystems are ephemeral, so without it every redeploy wipes all notes. +- Set `APP_URL` to the platform-assigned domain. +- Set `SERVER_ENCRYPTION_KEY` yourself — it must be 32 random bytes, base64 + encoded (`openssl rand -base64 32`). A platform's generic "random value" + generator won't match this format. + +--- + ## Synology NAS (DSM 7 / Container Manager) 1. **Install Container Manager** from the DSM Package Center (if not already). diff --git a/render.yaml b/render.yaml new file mode 100644 index 0000000..bef6fd2 --- /dev/null +++ b/render.yaml @@ -0,0 +1,22 @@ +# Render blueprint — one-click deploy of a Secret instance. +# Docs: https://render.com/docs/blueprint-spec +# +# After deploying, set the two `sync: false` env vars in the Render dashboard: +# - SERVER_ENCRYPTION_KEY : 32 random bytes, base64 (run: openssl rand -base64 32) +# NEVER change it after launch — notes become unreadable. +# - APP_URL : the public URL Render assigns (https://.onrender.com) +services: + - type: web + name: secret + runtime: image + image: + url: ghcr.io/largerio/secret:latest + envVars: + - key: SERVER_ENCRYPTION_KEY + sync: false + - key: APP_URL + sync: false + disk: + name: secret-data + mountPath: /app/data + sizeGB: 1 From f2fb51cf110780b4a1eb36cccc37b0613f771061 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 12:11:51 +0000 Subject: [PATCH 3/3] fix: correct regressions found while reviewing self-host changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docker-compose.yml: drop the env_file approach and restore the explicit environment allowlist. env_file injected PORT=3000 from .env into the container, overriding the Dockerfile's PORT=3001 for the API, so both API and web bound to 3000 (collision). It also dropped the BODY_SIZE_LIMIT default. - entrypoint.sh: default BODY_SIZE_LIMIT to ~101MB for the web server so the image accepts uploads out of the box for docker run / Render / Coolify-image deploys (adapter-node otherwise caps bodies at 512K). - docs/self-hosting.md: fix backup/restore — compose prefixes the volume with the project name (secret_secret-data), so document discovering the real name instead of the unprefixed secret-data. https://claude.ai/code/session_01WkTWZvbmaA6aUT5hQSTz9m --- docker-compose.yml | 19 +++++++++++++++---- docs/self-hosting.md | 33 +++++++++++++++++++++++---------- entrypoint.sh | 9 +++++++-- 3 files changed, 45 insertions(+), 16 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index ec60218..3dabea9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,13 +8,24 @@ services: - "${PORT:-3000}:3000" volumes: - secret-data:/app/data - # All configuration is read from the .env file (copy .env.example → .env). - env_file: - - .env environment: - # Force data paths into the persistent volume regardless of .env contents. - DATABASE_PATH=/app/data/secret.db - FILES_PATH=/app/data/files + - SERVER_ENCRYPTION_KEY=${SERVER_ENCRYPTION_KEY} + - APP_URL=${APP_URL:-http://localhost:3000} + - APP_NAME=${APP_NAME:-Secret} + - MAX_FILE_SIZE=${MAX_FILE_SIZE:-10485760} + - MAX_FILES_PER_NOTE=${MAX_FILES_PER_NOTE:-10} + - DEFAULT_EXPIRY=${DEFAULT_EXPIRY:-86400} + - MAX_EXPIRY=${MAX_EXPIRY:-604800} + - BODY_SIZE_LIMIT=${BODY_SIZE_LIMIT:-105906176} + - STORAGE_BACKEND=${STORAGE_BACKEND:-local} + - S3_BUCKET=${S3_BUCKET:-} + - S3_REGION=${S3_REGION:-us-east-1} + - S3_ENDPOINT=${S3_ENDPOINT:-} + - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID:-} + - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY:-} + - S3_FORCE_PATH_STYLE=${S3_FORCE_PATH_STYLE:-false} restart: unless-stopped init: true read_only: true diff --git a/docs/self-hosting.md b/docs/self-hosting.md index 3efc377..da4ef34 100644 --- a/docs/self-hosting.md +++ b/docs/self-hosting.md @@ -39,7 +39,7 @@ docker compose up -d ``` The container pulls `ghcr.io/largerio/secret:latest`, creates a persistent -`secret-data` volume, and serves the app on port `3000`. +Docker volume for your data, and serves the app on port `3000`. Open `http://:3000` and you're live. > ⚠️ **Never change `SERVER_ENCRYPTION_KEY` after the first launch** — all @@ -146,8 +146,18 @@ DSM → **Login Portal → Advanced → Reverse Proxy** → create a rule from ## Backup & restore -Everything lives in the `secret-data` Docker volume: the SQLite database -(`secret.db` + WAL files) and the encrypted `files/` directory. +Everything lives in one Docker volume: the SQLite database (`secret.db` + WAL +files) and the encrypted `files/` directory. + +Compose prefixes the volume with the project name (the folder you ran +`docker compose` from), so the real name is usually `secret_secret-data`. +Confirm the exact name first: + +```bash +docker volume ls --format '{{.Name}}' | grep secret-data +``` + +Use that name in place of `` below. ### Back up @@ -155,9 +165,9 @@ Everything lives in the `secret-data` Docker volume: the SQLite database # Stop the container first so the API checkpoints the WAL on SIGTERM docker compose stop -# Snapshot the volume into a tarball +# Snapshot the volume into a tarball (tar preserves the uid 1001 ownership) docker run --rm \ - -v secret-data:/data \ + -v :/data \ -v "$PWD":/backup \ alpine tar czf /backup/secret-backup.tgz -C /data . @@ -170,14 +180,17 @@ data is useless without the key. ### Restore ```bash -# Recreate the volume from the tarball, then start fresh -docker volume create secret-data +# Let compose create the container and its volume first… +docker compose up -d +docker compose stop + +# …then extract the backup into that same docker run --rm \ - -v secret-data:/data \ + -v :/data \ -v "$PWD":/backup \ - alpine sh -c "cd /data && tar xzf /backup/secret-backup.tgz" + alpine sh -c "cd /data && rm -rf ./* && tar xzf /backup/secret-backup.tgz" -docker compose up -d +docker compose start ``` Make sure the restored instance uses the **same** `SERVER_ENCRYPTION_KEY` as the diff --git a/entrypoint.sh b/entrypoint.sh index 0a87799..f7ce2cc 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -12,8 +12,13 @@ trap term TERM INT node apps/api/dist/index.js & api_pid=$! -# SvelteKit frontend (exposed, port 3000) -PORT=3000 ORIGIN="${APP_URL:-http://localhost:3000}" node apps/web/build/index.js & +# SvelteKit frontend (exposed, port 3000). BODY_SIZE_LIMIT caps the request body +# the adapter-node server accepts; without it the adapter defaults to 512K, which +# rejects uploads. Default to ~101MB (MAX_FILE_SIZE * MAX_FILES_PER_NOTE + 1MB) so +# the image works out of the box regardless of how env vars are wired. +PORT=3000 ORIGIN="${APP_URL:-http://localhost:3000}" \ + BODY_SIZE_LIMIT="${BODY_SIZE_LIMIT:-105906176}" \ + node apps/web/build/index.js & web_pid=$! # Exit as soon as either process exits, then signal the other and reap both.