Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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 ===
Expand All @@ -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

Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 }}
Expand Down
37 changes: 32 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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=<output>
# 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:
Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
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:
Expand Down
209 changes: 209 additions & 0 deletions docs/self-hosting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
# 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)
- [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)

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=<output> 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
Docker volume for your data, and serves the app on port `3000`.
Open `http://<your-host>: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.

---

## 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).
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://<NAS-IP>: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 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 `<volume>` below.

### Back up

```bash
# Stop the container first so the API checkpoints the WAL on SIGTERM
docker compose stop

# Snapshot the volume into a tarball (tar preserves the uid 1001 ownership)
docker run --rm \
-v <volume>:/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
# Let compose create the container and its volume first…
docker compose up -d
docker compose stop

# …then extract the backup into that same <volume>
docker run --rm \
-v <volume>:/data \
-v "$PWD":/backup \
alpine sh -c "cd /data && rm -rf ./* && tar xzf /backup/secret-backup.tgz"

docker compose start
```

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.
9 changes: 7 additions & 2 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
22 changes: 22 additions & 0 deletions render.yaml
Original file line number Diff line number Diff line change
@@ -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://<service>.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