Skip to content
Draft
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
40 changes: 40 additions & 0 deletions deploy/kubernetes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Kubernetes deployment

Reference manifests for running this Enclosed fork (WEIN & CO Notes) on Kubernetes.
See the full guide with explanations in [`docs: Self-hosting → Kubernetes`](../../packages/docs/src/self-hosting/kubernetes.md).

## Quick start

```bash
# 1. Build and push the branded image (the branding is baked in at build time)
docker build -t ghcr.io/predator2003/enclosed:v1.16.0-weinco .
docker push ghcr.io/predator2003/enclosed:v1.16.0-weinco

# 2. Create the secret
cp secret.example.yaml secret.yaml
# edit secret.yaml: set AUTHENTICATION_JWT_SECRET (openssl rand -base64 48)
# then uncomment "- secret.yaml" in kustomization.yaml

# 3. Adjust notes.example.com in ingress.yaml, then deploy
kubectl apply -k .

# 4. Check
kubectl -n enclosed get pods
kubectl -n enclosed logs deploy/enclosed
```

## Important operational notes

- **HTTPS is mandatory.** The client-side encryption uses the browser WebCrypto API,
which is only available in secure contexts. Plain HTTP will show a warning and
note creation will not work.
- **Exactly 1 replica.** The default fs-lite storage is a single-writer embedded
store on the PVC. Do not scale the Deployment horizontally.
- **Non-root:** the manifests run the pod as UID/GID 1000 with `fsGroup: 1000`.
`PUID`/`PGID` environment variables are not supported by the image.
- **Upload size:** the ingress `proxy-body-size` must be at least
`NOTES_MAX_ENCRYPTED_PAYLOAD_LENGTH` (default 50 MiB) plus headroom.
- **JWT secret:** the server refuses to start when authentication is enabled and
`AUTHENTICATION_JWT_SECRET` is still the default value.
- **Backups:** all notes are encrypted client-side; backing up the PVC never
exposes plaintext.
26 changes: 26 additions & 0 deletions deploy/kubernetes/configmap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: enclosed-config
namespace: enclosed
labels:
app.kubernetes.io/name: enclosed
data:
PORT: "8787"
# TLS is terminated at the ingress; the pod itself speaks plain HTTP.
SERVER_USE_HTTPS: "false"
# Storage path inside the container; the PVC is mounted there.
STORAGE_DRIVER_FS_LITE_PATH: "/app/.data"
# Maximum encrypted payload size in bytes (50 MiB). If you raise this, raise
# the ingress body-size limit as well (see ingress.yaml).
NOTES_MAX_ENCRYPTED_PAYLOAD_LENGTH: "52428800"
# Hourly cleanup of expired notes.
TASK_DELETE_EXPIRED_NOTES_ENABLED: "true"
TASK_DELETE_EXPIRED_NOTES_CRON: "0 * * * *"
TASK_DELETE_EXPIRED_NOTES_RUN_ON_STARTUP: "true"
# Default note lifetime (1 hour) and whether "never expires" is allowed.
PUBLIC_DEFAULT_NOTE_TTL_SECONDS: "3600"
PUBLIC_IS_SETTING_NO_EXPIRATION_ALLOWED: "false"
# Set to "true" (and provide AUTHENTICATION_USERS in the secret) to require
# login for creating notes.
PUBLIC_IS_AUTHENTICATION_REQUIRED: "false"
78 changes: 78 additions & 0 deletions deploy/kubernetes/deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: enclosed
namespace: enclosed
labels:
app.kubernetes.io/name: enclosed
spec:
# fs-lite storage on a ReadWriteOnce volume supports exactly one writer:
# keep replicas at 1 and use the Recreate strategy so the old pod releases
# the volume before the new one starts.
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: enclosed
template:
metadata:
labels:
app.kubernetes.io/name: enclosed
spec:
securityContext:
# The rootless image runs as UID/GID 1000; fsGroup makes the mounted
# volume group-writable for it (PUID/PGID env vars are NOT supported).
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: enclosed
# For the WEIN & CO branded build, build and push your own image from
# this repository (see deploy/kubernetes/README.md). The upstream
# image corentinth/enclosed:latest-rootless works but is unbranded.
image: ghcr.io/predator2003/enclosed:latest
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8787
envFrom:
- configMapRef:
name: enclosed-config
- secretRef:
name: enclosed-secrets
volumeMounts:
- name: data
mountPath: /app/.data
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
readinessProbe:
httpGet:
path: /api/ping
port: http
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /api/ping
port: http
initialDelaySeconds: 15
periodSeconds: 20
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
# Allow headroom for large encrypted payloads (50 MiB default cap).
memory: 512Mi
volumes:
- name: data
persistentVolumeClaim:
claimName: enclosed-data
33 changes: 33 additions & 0 deletions deploy/kubernetes/ingress.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: enclosed
namespace: enclosed
labels:
app.kubernetes.io/name: enclosed
annotations:
# HTTPS is REQUIRED: the client-side encryption uses the WebCrypto API,
# which browsers only expose in secure contexts.
cert-manager.io/cluster-issuer: letsencrypt-prod
# Must be >= NOTES_MAX_ENCRYPTED_PAYLOAD_LENGTH (50 MiB default) plus headroom,
# otherwise large notes/files fail at the ingress before reaching the app.
nginx.ingress.kubernetes.io/proxy-body-size: 64m
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
nginx.ingress.kubernetes.io/proxy-send-timeout: "60"
spec:
ingressClassName: nginx
tls:
- hosts:
- notes.example.com
secretName: enclosed-tls
rules:
- host: notes.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: enclosed
port:
name: http
17 changes: 17 additions & 0 deletions deploy/kubernetes/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- namespace.yaml
- configmap.yaml
# Copy secret.example.yaml to secret.yaml, fill in real values first:
# - secret.yaml
- pvc.yaml
- deployment.yaml
- service.yaml
- ingress.yaml

# Override the image in one place, e.g.:
# images:
# - name: ghcr.io/predator2003/enclosed
# newTag: v1.16.0-weinco
6 changes: 6 additions & 0 deletions deploy/kubernetes/namespace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: enclosed
labels:
app.kubernetes.io/name: enclosed
14 changes: 14 additions & 0 deletions deploy/kubernetes/pvc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: enclosed-data
namespace: enclosed
labels:
app.kubernetes.io/name: enclosed
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
# storageClassName: your-storage-class
20 changes: 20 additions & 0 deletions deploy/kubernetes/secret.example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copy to secret.yaml, fill in real values, then apply. NEVER commit real secrets.
#
# Generate a strong JWT secret:
# openssl rand -base64 48
#
# Generate the users list (email:bcrypt-hash pairs, comma separated):
# https://docs.enclosed.cc/self-hosting/users-authentication-key-generator
# or: htpasswd -bnBC 10 "" 'your-password' | tr -d ':\n'
apiVersion: v1
kind: Secret
metadata:
name: enclosed-secrets
namespace: enclosed
labels:
app.kubernetes.io/name: enclosed
type: Opaque
stringData:
AUTHENTICATION_JWT_SECRET: "REPLACE_ME__openssl_rand_base64_48"
# Only needed when PUBLIC_IS_AUTHENTICATION_REQUIRED is "true":
AUTHENTICATION_USERS: ""
15 changes: 15 additions & 0 deletions deploy/kubernetes/service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: enclosed
namespace: enclosed
labels:
app.kubernetes.io/name: enclosed
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: enclosed
ports:
- name: http
port: 80
targetPort: http
58 changes: 11 additions & 47 deletions packages/app-client/index.html
Original file line number Diff line number Diff line change
@@ -1,64 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<title>Enclosed - Send private and secure notes</title>
<meta name="theme-color" content="#BC0936" />
<title>WEIN & CO Notes – Vertrauliche Notizen sicher teilen</title>

<meta name="title" content="Enclosed - Send private and secure notes">
<meta name="description" content="Enclosed is a secure, end-to-end encrypted platform for sending private notes. Set expiration, passwords, and self-destruct options to keep your notes confidential.">

<link rel="author" href="humans.txt" />
<link rel="canonical" href="https://enclosed.cc/" />

<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://enclosed.cc/">
<meta property="og:title" content="Enclosed - Send private and secure notes">
<meta property="og:description" content="Enclosed is a secure, end-to-end encrypted platform for sending private notes. Set expiration, passwords, and self-destruct options to keep your notes confidential.">
<meta property="og:image" content="https://enclosed.cc/og-image.png">

<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="https://enclosed.cc/">
<meta property="twitter:title" content="Enclosed - Send private and secure notes">
<meta property="twitter:description" content="Enclosed is a secure, end-to-end encrypted platform for sending private notes. Set expiration, passwords, and self-destruct options to keep your notes confidential.">
<meta property="twitter:image" content="https://enclosed.cc/og-image.png">
<meta name="title" content="WEIN & CO Notes – Vertrauliche Notizen sicher teilen">
<meta name="description" content="WEIN & CO Notes ist eine Ende-zu-Ende-verschlüsselte Plattform zum sicheren Teilen vertraulicher Notizen und Dateien. Mit Ablaufdatum, Passwortschutz und Selbstzerstörung nach dem Lesen.">
<meta name="robots" content="noindex, nofollow">

<!-- Favicon and Icons -->
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<link rel="apple-touch-icon" sizes="152x152" href="/apple-touch-icon.png">
<link rel="manifest" href="/site.webmanifest">

<!-- Structured Data (JSON-LD for rich snippets) -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "Enclosed",
"url": "https://enclosed.cc/",
"description": "Send private and secure notes with end-to-end encryption. Enclosed offers configurable security features like passwords, expiration times, and self-destruction after reading.",
"applicationCategory": "Utilities",
"operatingSystem": "Web",
"browserRequirements": "Requires JavaScript",
"image": "https://enclosed.cc/og-image.png",
"creator": {
"@type": "Organization",
"name": "Enclosed"
}
}
</script>

<style>.sr-only {position: absolute;width: 1px;height: 1px;padding: 0;margin: -1px;overflow: hidden;clip: rect(0, 0, 0, 0);white-space: nowrap;border-width: 0;}</style>
</head>
<body>
<h1 class="sr-only">Enclosed - Send Private and Secure Notes</h1>
<p class="sr-only">Enclosed is a secure platform for sending private notes. All notes are end-to-end encrypted with zero knowledge on the server side.</p>
<h1 class="sr-only">WEIN & CO Notes – Vertrauliche Notizen sicher teilen</h1>
<p class="sr-only">WEIN & CO Notes ist eine sichere Plattform zum Teilen vertraulicher Notizen. Alle Notizen sind Ende-zu-Ende verschlüsselt – der Server kennt den Inhalt nicht.</p>

<noscript>You need to enable JavaScript to run this app.</noscript>
<noscript>Bitte aktivieren Sie JavaScript, um diese Anwendung zu nutzen.</noscript>
<div id="root"></div>

<script src="/src/index.tsx" type="module"></script>
Expand Down
5 changes: 3 additions & 2 deletions packages/app-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"dependencies": {
"@corentinth/chisels": "catalog:",
"@enclosed/lib": "workspace:*",
"@fontsource/mulish": "^5.2.8",
"@kobalte/core": "^0.13.4",
"@solid-primitives/i18n": "^2.1.1",
"@solid-primitives/storage": "^4.2.1",
Expand All @@ -38,7 +39,7 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"jszip": "^3.10.1",
"lodash-es": "^4.17.21",
"lodash-es": "^4.18.1",
"solid-js": "^1.9.5",
"solid-sonner": "^0.2.8",
"tailwind-merge": "^2.5.2",
Expand All @@ -56,7 +57,7 @@
"tsx": "^4.19.3",
"typescript": "catalog:",
"unocss": "^0.64.0",
"vite": "^5.0.11",
"vite": "^5.4.21",
"vite-plugin-solid": "^2.11.6",
"vitest": "catalog:"
}
Expand Down
Binary file not shown.
Binary file not shown.
Binary file modified packages/app-client/public/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed packages/app-client/public/favicon-16x16.png
Binary file not shown.
Binary file removed packages/app-client/public/favicon-32x32.png
Binary file not shown.
Binary file modified packages/app-client/public/favicon.ico
Binary file not shown.
9 changes: 6 additions & 3 deletions packages/app-client/public/humans.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
/* TEAM */
Developer: Corentin Thomasset
Site: https://corentin.tech
Twitter: @cthmsst
Organization: WEIN & CO Handelsgesellschaft m.b.H.
Site: https://www.weinco.at

/* THANKS */
Based on Enclosed by Corentin Thomasset (Apache 2.0)
Site: https://github.com/CorentinTh/enclosed
Binary file added packages/app-client/public/icon-196.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading