From d1e3be42c850a6f9e3c15be8c513d980e6f674cb Mon Sep 17 00:00:00 2001 From: Raphael Nussbaumer Date: Mon, 1 Sep 2025 07:36:35 +0200 Subject: [PATCH 1/7] wip --- .vscode/settings.json | 17 +++ backend/.dockerignore | 1 + backend/drizzle/0000_warm_blink.sql | 49 ++++++++ .../drizzle/0001_migrate_to_bigint_ids.sql | 91 --------------- backend/drizzle/meta/0000_snapshot.json | 21 +++- backend/drizzle/meta/_journal.json | 8 +- backend/src/endpoints/getPresignedUrls.ts | 16 ++- dockerimage/.env.example | 16 +++ dockerimage/Dockerfile | 89 +++++++++++++++ dockerimage/README.md | 19 +++ dockerimage/docker-compose.yml | 17 +++ dockerimage/nginx.conf | 50 ++++++++ dockerimage/start.sh | 108 ++++++++++++++++++ frontend/.dockerignore | 9 ++ 14 files changed, 410 insertions(+), 101 deletions(-) create mode 100644 backend/drizzle/0000_warm_blink.sql delete mode 100644 backend/drizzle/0001_migrate_to_bigint_ids.sql create mode 100644 dockerimage/.env.example create mode 100644 dockerimage/Dockerfile create mode 100644 dockerimage/README.md create mode 100644 dockerimage/docker-compose.yml create mode 100644 dockerimage/nginx.conf create mode 100644 dockerimage/start.sh create mode 100644 frontend/.dockerignore diff --git a/.vscode/settings.json b/.vscode/settings.json index 638b3b4..a57879e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,14 +22,17 @@ "clipb", "comlink", "Consolas", + "createdb", "CSIC", "CTAN", "customtabs", "Dailymotion", "dasharray", "dashoffset", + "datname", "dexie", "Dexie", + "dockerimage", "downl", "DSGVO", "Ecosia", @@ -48,6 +51,7 @@ "jszip", "keyalg", "keymap", + "keypair", "keysize", "keytool", "Kotaku", @@ -59,10 +63,17 @@ "matplotlib", "Metacritic", "nginx", + "nocrypt", "Nußbaumer", + "nvmrc", + "openssl", "outdir", + "outform", "panzoom", "passwortloser", + "PGDATA", + "pkcs", + "PKCS", "postgres", "postgresql", "precache", @@ -70,10 +81,13 @@ "presign", "presigner", "printcert", + "psql", + "pubout", "qrcode", "Qwant", "replit", "rgba", + "rolname", "Searchalot", "sels", "serverside", @@ -83,9 +97,12 @@ "Supportanfragen", "Swisscows", "tabler", + "topk", "unarch", "urlset", "Usec", + "uuid", + "Vite", "Wickenburggasse", "yudiel" ], diff --git a/backend/.dockerignore b/backend/.dockerignore index dd269be..b1a05e3 100644 --- a/backend/.dockerignore +++ b/backend/.dockerignore @@ -1,5 +1,6 @@ dist node_modules +.bun-version .dockerignore .env .env.example diff --git a/backend/drizzle/0000_warm_blink.sql b/backend/drizzle/0000_warm_blink.sql new file mode 100644 index 0000000..99dce15 --- /dev/null +++ b/backend/drizzle/0000_warm_blink.sql @@ -0,0 +1,49 @@ +CREATE TYPE "public"."note_type" AS ENUM('note', 'todo', 'label', 'file');--> statement-breakpoint +CREATE TYPE "public"."subscription_type" AS ENUM('free', 'plus', 'pro');--> statement-breakpoint +CREATE TABLE "notes" ( + "id" bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "notes_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "user_id" bigint NOT NULL, + "clientside_id" varchar(36) NOT NULL, + "type" "note_type" DEFAULT 'note' NOT NULL, + "cipher_text" text, + "iv" varchar(16), + "version" integer DEFAULT 1 NOT NULL, + "serverside_created_at" bigint NOT NULL, + "serverside_updated_at" bigint NOT NULL, + "clientside_created_at" bigint NOT NULL, + "clientside_updated_at" bigint NOT NULL, + "clientside_deleted_at" bigint, + "committed_size" integer DEFAULT 0 NOT NULL, + CONSTRAINT "user_client_id" UNIQUE("user_id","clientside_id") +); +--> statement-breakpoint +CREATE TABLE "sessions" ( + "id" bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "sessions_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "user_id" bigint NOT NULL, + "access_token_hash" varchar(64) NOT NULL, + "access_token_salt" varchar(32) NOT NULL, + "created_at" bigint NOT NULL +); +--> statement-breakpoint +CREATE TABLE "users" ( + "id" bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "users_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "email" varchar(255) NOT NULL, + "password_hash" varchar(255), + "is_admin" boolean DEFAULT false NOT NULL, + "login_code" varchar(6), + "login_code_created_at" bigint, + "login_tries_left" integer DEFAULT 0 NOT NULL, + "created_at" bigint NOT NULL, + "updated_at" bigint NOT NULL, + "sync_token" varchar(24), + "confirm_code" varchar(6), + "confirm_code_created_at" bigint, + "confirm_code_tries_left" integer DEFAULT 0 NOT NULL, + "new_email" varchar(255), + "subscription" "subscription_type" DEFAULT 'free' NOT NULL, + "successful_login_at" bigint, + CONSTRAINT "users_email_unique" UNIQUE("email") +); +--> statement-breakpoint +ALTER TABLE "notes" ADD CONSTRAINT "notes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action; \ No newline at end of file diff --git a/backend/drizzle/0001_migrate_to_bigint_ids.sql b/backend/drizzle/0001_migrate_to_bigint_ids.sql deleted file mode 100644 index 0e5393d..0000000 --- a/backend/drizzle/0001_migrate_to_bigint_ids.sql +++ /dev/null @@ -1,91 +0,0 @@ --- Migration to convert integer IDs to bigint IDs while preserving data --- This migration handles the conversion safely without data loss - --- Step 1: Create new tables with bigint IDs -CREATE TABLE "users_new" ( - "id" bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "users_new_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), - "email" varchar(255) NOT NULL, - "login_code" varchar(6), - "login_code_created_at" bigint, - "login_tries_left" integer DEFAULT 0 NOT NULL, - "created_at" bigint NOT NULL, - "updated_at" bigint NOT NULL, - "sync_token" varchar(24), - "confirm_code" varchar(6), - "confirm_code_created_at" bigint, - "confirm_code_tries_left" integer DEFAULT 0 NOT NULL, - "new_email" varchar(255), - "subscription" "subscription_type" DEFAULT 'free' NOT NULL, - CONSTRAINT "users_new_email_unique" UNIQUE("email") -); - -CREATE TABLE "sessions_new" ( - "id" bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "sessions_new_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), - "user_id" bigint NOT NULL, - "access_token_hash" varchar(64) NOT NULL, - "access_token_salt" varchar(32) NOT NULL, - "created_at" bigint NOT NULL -); - -CREATE TABLE "notes_new" ( - "id" bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "notes_new_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), - "user_id" bigint NOT NULL, - "clientside_id" varchar(36) NOT NULL, - "type" "note_type" DEFAULT 'note' NOT NULL, - "cipher_text" text, - "iv" varchar(16), - "version" integer DEFAULT 1 NOT NULL, - "serverside_created_at" bigint NOT NULL, - "serverside_updated_at" bigint NOT NULL, - "clientside_created_at" bigint NOT NULL, - "clientside_updated_at" bigint NOT NULL, - "clientside_deleted_at" bigint, - "committed_size" integer DEFAULT 0 NOT NULL, - CONSTRAINT "user_client_id_new" UNIQUE("user_id","clientside_id") -); - --- Step 2: Copy data from old tables to new tables with OVERRIDING SYSTEM VALUE -INSERT INTO "users_new" ("id", "email", "login_code", "login_code_created_at", "login_tries_left", "created_at", "updated_at", "sync_token", "confirm_code", "confirm_code_created_at", "confirm_code_tries_left", "new_email", "subscription") -OVERRIDING SYSTEM VALUE -SELECT "id", "email", "login_code", "login_code_created_at", "login_tries_left", "created_at", "updated_at", "sync_token", "confirm_code", "confirm_code_created_at", "confirm_code_tries_left", "new_email", "subscription" -FROM "users"; - -INSERT INTO "sessions_new" ("id", "user_id", "access_token_hash", "access_token_salt", "created_at") -OVERRIDING SYSTEM VALUE -SELECT "id", "user_id", "access_token_hash", "access_token_salt", "created_at" -FROM "sessions"; - -INSERT INTO "notes_new" ("id", "user_id", "clientside_id", "type", "cipher_text", "iv", "version", "serverside_created_at", "serverside_updated_at", "clientside_created_at", "clientside_updated_at", "clientside_deleted_at", "committed_size") -OVERRIDING SYSTEM VALUE -SELECT "id", "user_id", "clientside_id", "type", "cipher_text", "iv", "version", "serverside_created_at", "serverside_updated_at", "clientside_created_at", "clientside_updated_at", "clientside_deleted_at", "committed_size" -FROM "notes"; - --- Step 3: Set sequence values to continue from the highest ID -SELECT setval('users_new_id_seq', (SELECT COALESCE(MAX(id), 0) + 1 FROM users_new)); -SELECT setval('sessions_new_id_seq', (SELECT COALESCE(MAX(id), 0) + 1 FROM sessions_new)); -SELECT setval('notes_new_id_seq', (SELECT COALESCE(MAX(id), 0) + 1 FROM notes_new)); - --- Step 4: Add foreign key constraints to new tables -ALTER TABLE "sessions_new" ADD CONSTRAINT "sessions_new_user_id_users_new_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users_new"("id") ON DELETE no action ON UPDATE no action; -ALTER TABLE "notes_new" ADD CONSTRAINT "notes_new_user_id_users_new_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users_new"("id") ON DELETE no action ON UPDATE no action; - --- Step 5: Drop old tables -DROP TABLE "notes"; -DROP TABLE "sessions"; -DROP TABLE "users"; - --- Step 6: Rename new tables to original names -ALTER TABLE "users_new" RENAME TO "users"; -ALTER TABLE "sessions_new" RENAME TO "sessions"; -ALTER TABLE "notes_new" RENAME TO "notes"; - --- Step 7: Rename constraints to original names -ALTER TABLE "users" RENAME CONSTRAINT "users_new_email_unique" TO "users_email_unique"; -ALTER TABLE "sessions" RENAME CONSTRAINT "sessions_new_user_id_users_new_id_fk" TO "sessions_user_id_users_id_fk"; -ALTER TABLE "notes" RENAME CONSTRAINT "notes_new_user_id_users_new_id_fk" TO "notes_user_id_users_id_fk"; -ALTER TABLE "notes" RENAME CONSTRAINT "user_client_id_new" TO "user_client_id"; - --- Step 8: Rename sequences to original names -ALTER SEQUENCE "users_new_id_seq" RENAME TO "users_id_seq"; -ALTER SEQUENCE "sessions_new_id_seq" RENAME TO "sessions_id_seq"; -ALTER SEQUENCE "notes_new_id_seq" RENAME TO "notes_id_seq"; \ No newline at end of file diff --git a/backend/drizzle/meta/0000_snapshot.json b/backend/drizzle/meta/0000_snapshot.json index 40fd29b..07b2eb4 100644 --- a/backend/drizzle/meta/0000_snapshot.json +++ b/backend/drizzle/meta/0000_snapshot.json @@ -1,5 +1,5 @@ { - "id": "30ed7634-b16d-4176-b067-561c5f6d48a9", + "id": "b828c258-78b3-4758-b8f7-9fda1add2a07", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", @@ -228,6 +228,19 @@ "primaryKey": false, "notNull": true }, + "password_hash": { + "name": "password_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, "login_code": { "name": "login_code", "type": "varchar(6)", @@ -297,6 +310,12 @@ "primaryKey": false, "notNull": true, "default": "'free'" + }, + "successful_login_at": { + "name": "successful_login_at", + "type": "bigint", + "primaryKey": false, + "notNull": false } }, "indexes": {}, diff --git a/backend/drizzle/meta/_journal.json b/backend/drizzle/meta/_journal.json index 1723a85..88ce3ed 100644 --- a/backend/drizzle/meta/_journal.json +++ b/backend/drizzle/meta/_journal.json @@ -1,12 +1,12 @@ { - "version": "5", + "version": "7", "dialect": "postgresql", "entries": [ { "idx": 0, - "version": "5", - "when": 1732487460000, - "tag": "0001_migrate_to_bigint_ids", + "version": "7", + "when": 1756705234111, + "tag": "0000_warm_blink", "breakpoints": true } ] diff --git a/backend/src/endpoints/getPresignedUrls.ts b/backend/src/endpoints/getPresignedUrls.ts index fceea1b..6bf3bbd 100644 --- a/backend/src/endpoints/getPresignedUrls.ts +++ b/backend/src/endpoints/getPresignedUrls.ts @@ -5,7 +5,7 @@ import {notesTbl} from '../db/schema' import {and, eq, gt, inArray, isNull} from 'drizzle-orm' import {s3} from '../services/s3' import {indexByProp} from '../util/misc' -import {env} from '../env' +import {env, hostingMode} from '../env' import {GetObjectCommand} from '@aws-sdk/client-s3' import {getSignedUrl} from '@aws-sdk/s3-request-presigner' import {createPresignedPost} from '@aws-sdk/s3-presigned-post' @@ -21,7 +21,7 @@ export const getPresignedUrlsEndpoint = authEndpointsFactory.build({ upload_urls: z.array( z.object({ note_id: z.string().uuid(), - url: z.string().url(), + url: z.string(), fields: z.record(z.string(), z.string()), }) ), @@ -29,7 +29,7 @@ export const getPresignedUrlsEndpoint = authEndpointsFactory.build({ download_urls: z.array( z.object({ note_id: z.string().uuid(), - url: z.string().url(), + url: z.string(), }) ), }), @@ -116,7 +116,10 @@ const getUploadUrls = async ( }) return { note_id: note.clientside_id, - url, + url: + hostingMode !== 'self' + ? url + : url.replace(/http:\/\/(localhost|127\.0\.0\.1|::1):9000/, '/s3'), fields, } }) @@ -151,7 +154,10 @@ const getDownloadUrls = async (note_ids: string[], user_id: number) => { ) return { note_id: note.clientside_id, - url, + url: + hostingMode !== 'self' + ? url + : url.replace(/http:\/\/(localhost|127\.0\.0\.1|::1):9000/, '/s3'), } }) ) diff --git a/dockerimage/.env.example b/dockerimage/.env.example new file mode 100644 index 0000000..cd15017 --- /dev/null +++ b/dockerimage/.env.example @@ -0,0 +1,16 @@ +# Minimal config for self-hosted single-container setup + +ADMIN_USERNAME=admin +ADMIN_PASSWORD=TODO + +# Security (change in production, use a uuid) +COOKIE_SECRET=TODO + +# Optional overrides +# PORT=5100 +# RATE_WINDOW_SEC=60 +# RATE_LIMIT=200 +# SESSION_TTL_MIN=43200 +# NOTES_STORAGE_LIMIT=1000000000 +# FILES_STORAGE_LIMIT=1000000000 +# S3_BUCKET=ciphernotes diff --git a/dockerimage/Dockerfile b/dockerimage/Dockerfile new file mode 100644 index 0000000..5261c57 --- /dev/null +++ b/dockerimage/Dockerfile @@ -0,0 +1,89 @@ +######################################## +# Keys stage: generate JWT keypair +######################################## +FROM alpine:3.20 AS keys +RUN apk add --no-cache openssl +RUN openssl genrsa -out /tmp/private_rsa.pem 2048 \ + && openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in /tmp/private_rsa.pem -out /tmp/private_pkcs8.pem \ + && openssl rsa -in /tmp/private_rsa.pem -pubout -out /tmp/public.pem + +######################################## +# Frontend build stage +######################################## +FROM node:22-alpine AS frontend-build +WORKDIR /app/frontend +RUN apk add --no-cache openssl +COPY frontend/package.json frontend/yarn.lock ./ +RUN yarn install --frozen-lockfile +COPY frontend/ . +# Use generated public key so the static bundle verifies backend JWTs +COPY --from=keys /tmp/public.pem ./jwt-public.pub +# Build-time env for Vite defines +ARG HOSTING_MODE=self +ENV NODE_ENV=production \ + HOSTING_MODE=$HOSTING_MODE +RUN yarn build + +######################################## +# Backend build stage +######################################## +FROM oven/bun:1 AS backend-build +WORKDIR /app/backend +COPY backend/package.json backend/bun.lockb ./ +RUN bun install --frozen-lockfile +COPY backend/ . +RUN bun run build + +######################################## +# Runtime stage: bun + nginx +######################################## +FROM oven/bun:1 AS app +WORKDIR /app + +# Install nginx, Postgres and tools +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + nginx postgresql postgresql-client curl ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Install MinIO server and client (mc) +RUN curl -sSL -o /usr/local/bin/minio https://dl.min.io/server/minio/release/linux-amd64/minio \ + && chmod +x /usr/local/bin/minio \ + && curl -sSL -o /usr/local/bin/mc https://dl.min.io/client/mc/release/linux-amd64/mc \ + && chmod +x /usr/local/bin/mc + +# Overwrite frontend public key with generated one and then copy build output +COPY --from=keys /tmp/public.pem /app/frontend/jwt-public.pub +COPY --from=frontend-build /app/frontend/dist /usr/share/nginx/html + +# Copy backend runtime bits +COPY --from=backend-build /app/backend/dist /app/backend/dist +COPY --from=backend-build /app/backend/package.json /app/backend/package.json +# Copy backend node_modules so we can run drizzle-kit at runtime +COPY --from=backend-build /app/backend/node_modules /app/backend/node_modules +# Copy drizzle config and migrations +COPY --from=backend-build /app/backend/drizzle /app/backend/drizzle +COPY --from=backend-build /app/backend/drizzle.config.ts /app/backend/drizzle.config.ts +# Copy backend source (for drizzle schema path) +COPY --from=backend-build /app/backend/src /app/backend/src +COPY --from=backend-build /app/backend/tsconfig.json /app/backend/tsconfig.json +# Use generated private key (PKCS8) for backend JWT signing +COPY --from=keys /tmp/private_pkcs8.pem /app/backend/jwt-private.pem + +# Nginx config for SPA + backend proxy +COPY dockerimage/nginx.conf /etc/nginx/conf.d/default.conf + +# Entrypoint script to run both backend and nginx +COPY dockerimage/start.sh /start.sh +RUN chmod +x /start.sh + +# Persistent data location +VOLUME ["/data"] + +EXPOSE 80 + +ENV NODE_ENV=production + +CMD ["/start.sh"] + + diff --git a/dockerimage/README.md b/dockerimage/README.md new file mode 100644 index 0000000..6bb83bc --- /dev/null +++ b/dockerimage/README.md @@ -0,0 +1,19 @@ +Self-hosted bundle +================== + +Build and run everything with a single image plus Postgres and MinIO, persisting data to ./data. + +Usage +----- + +1) Copy `.env.example` to `.env` and adjust values. +2) Run: `docker compose -f dockerimage/docker-compose.yml up --build -d` +3) Open http://localhost:8080 + +Environment +----------- + +- All persistent data is stored under `./data` on the host and mounted at `/data` in containers. +- The app runs in self-hosting mode with pro features for all users. + + diff --git a/dockerimage/docker-compose.yml b/dockerimage/docker-compose.yml new file mode 100644 index 0000000..f3c726d --- /dev/null +++ b/dockerimage/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3.9' +services: + app: + build: + context: .. + dockerfile: dockerimage/Dockerfile + container_name: ciphernotes_app + env_file: + - .env + ports: + - "8080:80" + # To persist /data on host, uncomment the next line + # volumes: + # - ./data:/data + restart: unless-stopped + + diff --git a/dockerimage/nginx.conf b/dockerimage/nginx.conf new file mode 100644 index 0000000..271f6b4 --- /dev/null +++ b/dockerimage/nginx.conf @@ -0,0 +1,50 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # API proxy to backend bun server + location /api/ { + proxy_pass http://127.0.0.1:5100/; + proxy_http_version 1.1; + 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; + proxy_set_header Connection ""; + } + + # Socket.IO proxy + location /socket.io/ { + proxy_pass http://127.0.0.1:5100/socket.io/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + 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; + } + + # minio proxy (preserve Host used in pre-signed URLs) + location /s3/ { + proxy_pass http://127.0.0.1:9000/; + proxy_http_version 1.1; + proxy_set_header Host 127.0.0.1:9000; + 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; + proxy_set_header Connection ""; + # helpful for large uploads to presigned POST + client_max_body_size 0; + proxy_request_buffering off; + } + + location / { + try_files $uri $uri/ /index.html; + } +} + + diff --git a/dockerimage/start.sh b/dockerimage/start.sh new file mode 100644 index 0000000..08b3070 --- /dev/null +++ b/dockerimage/start.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Prepare data directories +mkdir -p /data/postgres /data/minio + +############################################ +# Defaults for required backend env vars +############################################ +export NODE_ENV=${NODE_ENV:-production} +export HOSTING_MODE=${HOSTING_MODE:-self} +export PORT=${PORT:-5100} +export TRUST_PROXY=${TRUST_PROXY:-1} +export RATE_WINDOW_SEC=${RATE_WINDOW_SEC:-60} +export RATE_LIMIT=${RATE_LIMIT:-200} +export SESSION_TTL_MIN=${SESSION_TTL_MIN:-43200} +export COOKIE_SECRET=${COOKIE_SECRET:-dev-cookie-secret} +export LIMIT_JSON=${LIMIT_JSON:-1mb} +export LIMIT_RAW=${LIMIT_RAW:-10mb} +export NOTES_STORAGE_LIMIT=${NOTES_STORAGE_LIMIT:-1000000000} +export FILES_STORAGE_LIMIT=${FILES_STORAGE_LIMIT:-1000000000} + +############################################ +# Database URL default (internal Postgres) +############################################ +export DATABASE_URL=${DATABASE_URL:-postgresql://notes:notes@localhost:5432/notes} + +############################################ +# MinIO/S3 defaults (internal MinIO) +############################################ +export MINIO_ROOT_USER=${MINIO_ROOT_USER:-minio-admin} +export MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-minio-admin} +export S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID:-$MINIO_ROOT_USER} +export S3_ACCESS_KEY_SECRET=${S3_ACCESS_KEY_SECRET:-$MINIO_ROOT_PASSWORD} +export S3_REGION=${S3_REGION:-EU-CENTRAL-1} +export S3_ENDPOINT=${S3_ENDPOINT:-http://127.0.0.1:9000} +export S3_BUCKET=${S3_BUCKET:-ciphernotes} + +############################################ +# Start Postgres +############################################ +export PGDATA=/data/postgres +chown -R postgres:postgres "$PGDATA" +chmod 700 "$PGDATA" || true +if [ ! -s "$PGDATA/PG_VERSION" ]; then + echo "Initializing Postgres data directory..." + su -s /bin/sh postgres -c "/usr/lib/postgresql/*/bin/initdb -D $PGDATA -E UTF8 --no-locale" + # Relax auth for localhost + echo "local all all trust" >> "$PGDATA/pg_hba.conf" + echo "host all all 127.0.0.1/32 trust" >> "$PGDATA/pg_hba.conf" + echo "host all all ::1/128 trust" >> "$PGDATA/pg_hba.conf" +fi +# Start postgres directly bound to localhost +su -s /bin/sh postgres -c "/usr/lib/postgresql/*/bin/postgres -D $PGDATA -c listen_addresses=localhost" & +POSTGRES_PID=$! + +# Wait for Postgres up +until pg_isready -q -h 127.0.0.1 -p 5432; do echo "Waiting for Postgres..."; sleep 1; done + +# Ensure database exists (connect over TCP to avoid peer auth) +if ! su -s /bin/sh postgres -c "psql -h 127.0.0.1 -p 5432 -tc \"SELECT 1 FROM pg_roles WHERE rolname='notes'\"" | grep -q 1; then + su -s /bin/sh postgres -c "psql -h 127.0.0.1 -p 5432 -c \"CREATE ROLE notes LOGIN PASSWORD 'notes';\"" +fi +if ! su -s /bin/sh postgres -c "psql -h 127.0.0.1 -p 5432 -tc \"SELECT 1 FROM pg_database WHERE datname='notes'\"" | grep -q 1; then + su -s /bin/sh postgres -c "createdb -h 127.0.0.1 -p 5432 -O notes notes" +else + su -s /bin/sh postgres -c "psql -h 127.0.0.1 -p 5432 -c \"ALTER DATABASE notes OWNER TO notes;\"" || true +fi + +############################################ +# Start MinIO +############################################ +mkdir -p /data/minio +minio server /data/minio --console-address ":9001" & +MINIO_PID=$! + +# Wait for MinIO up +until curl -fsS http://127.0.0.1:9000/minio/health/ready >/dev/null 2>&1; do echo "Waiting for MinIO..."; sleep 1; done + +# Create bucket if not exists +mc alias set local http://127.0.0.1:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" +mc ls local/${S3_BUCKET} || mc mb -p local/${S3_BUCKET} + +############################################ +# Apply migrations only +############################################ +cd /app/backend +echo "Applying migrations..." +./node_modules/.bin/drizzle-kit migrate || bun run db:migrate || true + +############################################ +# Start backend +############################################ +bun /app/backend/dist/index.js & +BACKEND_PID=$! + +############################################ +# Start nginx (foreground) +############################################ +# Ensure no default site conflicts (keep our conf in conf.d) +rm -f /etc/nginx/sites-enabled/default 2>/dev/null || true +nginx -g 'daemon off;' & +NGINX_PID=$! + +wait -n $BACKEND_PID $NGINX_PID $MINIO_PID $POSTGRES_PID +exit $? + + diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..a2ef496 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,9 @@ +dist +node_modules +.dockerignore +.env +.env.example +.nvmrc +cert.pem +Dockerfile +key.pem From e6746f95119f67b1e70f230dd28f52d3dad3950f Mon Sep 17 00:00:00 2001 From: Raphael Nussbaumer Date: Tue, 2 Sep 2025 10:16:10 +0200 Subject: [PATCH 2/7] wip --- dockerimage/.env.example | 1 - dockerimage/Dockerfile | 2 +- dockerimage/docker-compose.yml | 8 ++++---- dockerimage/nginx.conf | 21 ++++++++++++++------- todo.txt | 6 ++---- 5 files changed, 21 insertions(+), 17 deletions(-) diff --git a/dockerimage/.env.example b/dockerimage/.env.example index cd15017..3814042 100644 --- a/dockerimage/.env.example +++ b/dockerimage/.env.example @@ -13,4 +13,3 @@ COOKIE_SECRET=TODO # SESSION_TTL_MIN=43200 # NOTES_STORAGE_LIMIT=1000000000 # FILES_STORAGE_LIMIT=1000000000 -# S3_BUCKET=ciphernotes diff --git a/dockerimage/Dockerfile b/dockerimage/Dockerfile index 5261c57..aa99429 100644 --- a/dockerimage/Dockerfile +++ b/dockerimage/Dockerfile @@ -1,7 +1,7 @@ ######################################## # Keys stage: generate JWT keypair ######################################## -FROM alpine:3.20 AS keys +FROM alpine:3 AS keys RUN apk add --no-cache openssl RUN openssl genrsa -out /tmp/private_rsa.pem 2048 \ && openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in /tmp/private_rsa.pem -out /tmp/private_pkcs8.pem \ diff --git a/dockerimage/docker-compose.yml b/dockerimage/docker-compose.yml index f3c726d..e3f35f5 100644 --- a/dockerimage/docker-compose.yml +++ b/dockerimage/docker-compose.yml @@ -9,9 +9,9 @@ services: - .env ports: - "8080:80" - # To persist /data on host, uncomment the next line - # volumes: - # - ./data:/data + volumes: + - ciphernotes_data:/data restart: unless-stopped - +volumes: + ciphernotes_data: diff --git a/dockerimage/nginx.conf b/dockerimage/nginx.conf index 271f6b4..c9bc996 100644 --- a/dockerimage/nginx.conf +++ b/dockerimage/nginx.conf @@ -28,16 +28,25 @@ server { proxy_set_header X-Forwarded-Proto $scheme; } - # minio proxy (preserve Host used in pre-signed URLs) - location /s3/ { - proxy_pass http://127.0.0.1:9000/; + # Allow only GET/HEAD/POST to your bucket; block management/listing + location = /s3 { return 403; } + location = /s3/ { return 403; } + + location ~ ^/s3/ciphernotes(/|$) { + # Only allow browser flows + limit_except GET HEAD POST { deny all; } + + # Strip "/s3" prefix so MinIO sees "/ciphernotes/..." + rewrite ^/s3/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:9000; proxy_http_version 1.1; - proxy_set_header Host 127.0.0.1:9000; + proxy_set_header Host 127.0.0.1:9000; # keep SigV4 host consistent 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; proxy_set_header Connection ""; - # helpful for large uploads to presigned POST + + # presigned POST uploads client_max_body_size 0; proxy_request_buffering off; } @@ -46,5 +55,3 @@ server { try_files $uri $uri/ /index.html; } } - - diff --git a/todo.txt b/todo.txt index 1af5359..e20889d 100644 --- a/todo.txt +++ b/todo.txt @@ -60,10 +60,8 @@ DONE: on change encryption key delete all blobs with the old key FIRST: - confirm flow via password - three tries every 5 minutes - reset to 3 tries after 5 minutes - hcaptcha site code via env TODO: + error if minio password is not set or too short + make features pem optional LATER: MAYBE: From 5eb5df182c04463a634315a097d91f0dd07da283 Mon Sep 17 00:00:00 2001 From: Raphael Nussbaumer Date: Tue, 2 Sep 2025 10:57:13 +0200 Subject: [PATCH 3/7] generate minio root password --- dockerimage/start.sh | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/dockerimage/start.sh b/dockerimage/start.sh index 08b3070..c163ed6 100644 --- a/dockerimage/start.sh +++ b/dockerimage/start.sh @@ -28,8 +28,19 @@ export DATABASE_URL=${DATABASE_URL:-postgresql://notes:notes@localhost:5432/note ############################################ # MinIO/S3 defaults (internal MinIO) ############################################ +# If no password provided, read from file; if missing, generate and persist +MINIO_ROOT_PASSWORD_FILE=${MINIO_ROOT_PASSWORD_FILE:-/data/minio_root_password} +if [ -z "${MINIO_ROOT_PASSWORD:-}" ]; then + if [ -s "$MINIO_ROOT_PASSWORD_FILE" ]; then + MINIO_ROOT_PASSWORD="$(cat "$MINIO_ROOT_PASSWORD_FILE")" + else + MINIO_ROOT_PASSWORD="$(tr -dc 'A-Za-z0-9' "$MINIO_ROOT_PASSWORD_FILE" + chmod 600 "$MINIO_ROOT_PASSWORD_FILE" || true + fi +fi export MINIO_ROOT_USER=${MINIO_ROOT_USER:-minio-admin} -export MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-minio-admin} +export MINIO_ROOT_PASSWORD export S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID:-$MINIO_ROOT_USER} export S3_ACCESS_KEY_SECRET=${S3_ACCESS_KEY_SECRET:-$MINIO_ROOT_PASSWORD} export S3_REGION=${S3_REGION:-EU-CENTRAL-1} From 38255b0d21c85007a1e73861e5ae7123f0586e9a Mon Sep 17 00:00:00 2001 From: Raphael Nussbaumer Date: Tue, 2 Sep 2025 20:58:38 +0200 Subject: [PATCH 4/7] don't publish source --- dockerimage/Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dockerimage/Dockerfile b/dockerimage/Dockerfile index aa99429..92184c5 100644 --- a/dockerimage/Dockerfile +++ b/dockerimage/Dockerfile @@ -64,8 +64,7 @@ COPY --from=backend-build /app/backend/node_modules /app/backend/node_modules # Copy drizzle config and migrations COPY --from=backend-build /app/backend/drizzle /app/backend/drizzle COPY --from=backend-build /app/backend/drizzle.config.ts /app/backend/drizzle.config.ts -# Copy backend source (for drizzle schema path) -COPY --from=backend-build /app/backend/src /app/backend/src +# Copy only what's needed for migrations (avoid shipping source) COPY --from=backend-build /app/backend/tsconfig.json /app/backend/tsconfig.json # Use generated private key (PKCS8) for backend JWT signing COPY --from=keys /tmp/private_pkcs8.pem /app/backend/jwt-private.pem From 479e954778249af670d32e4c53ffc68375c4b27d Mon Sep 17 00:00:00 2001 From: Raphael Nussbaumer Date: Wed, 3 Sep 2025 07:53:37 +0200 Subject: [PATCH 5/7] multi container setup --- dockerimage/.env.example | 13 +++- dockerimage/Dockerfile | 13 +--- dockerimage/README.md | 130 ++++++++++++++++++++++++++++++--- dockerimage/docker-compose.yml | 57 ++++++++++++++- dockerimage/start.sh | 84 +++------------------ 5 files changed, 192 insertions(+), 105 deletions(-) diff --git a/dockerimage/.env.example b/dockerimage/.env.example index 3814042..ee10e60 100644 --- a/dockerimage/.env.example +++ b/dockerimage/.env.example @@ -1,13 +1,18 @@ -# Minimal config for self-hosted single-container setup - -ADMIN_USERNAME=admin +# Only the admin can create users. +# ou can use the admin use as regular app user. +ADMIN_USERNAME=TODO ADMIN_PASSWORD=TODO # Security (change in production, use a uuid) COOKIE_SECRET=TODO +# !!! choose secure credentials, anyone with you root password can read/write from/to the ciphernotes bucket !!! +# The s3 api is partially exposed through the /s3 proxy route. +MINIO_ROOT_USER=TODO +MINIO_ROOT_PASSWORD=TODO + # Optional overrides -# PORT=5100 +# TRUST_PROXY=3 # RATE_WINDOW_SEC=60 # RATE_LIMIT=200 # SESSION_TTL_MIN=43200 diff --git a/dockerimage/Dockerfile b/dockerimage/Dockerfile index 92184c5..1389684 100644 --- a/dockerimage/Dockerfile +++ b/dockerimage/Dockerfile @@ -40,18 +40,12 @@ RUN bun run build FROM oven/bun:1 AS app WORKDIR /app -# Install nginx, Postgres and tools +# Install nginx and curl RUN apt-get update \ && apt-get install -y --no-install-recommends \ - nginx postgresql postgresql-client curl ca-certificates \ + nginx curl ca-certificates \ && rm -rf /var/lib/apt/lists/* -# Install MinIO server and client (mc) -RUN curl -sSL -o /usr/local/bin/minio https://dl.min.io/server/minio/release/linux-amd64/minio \ - && chmod +x /usr/local/bin/minio \ - && curl -sSL -o /usr/local/bin/mc https://dl.min.io/client/mc/release/linux-amd64/mc \ - && chmod +x /usr/local/bin/mc - # Overwrite frontend public key with generated one and then copy build output COPY --from=keys /tmp/public.pem /app/frontend/jwt-public.pub COPY --from=frontend-build /app/frontend/dist /usr/share/nginx/html @@ -76,9 +70,6 @@ COPY dockerimage/nginx.conf /etc/nginx/conf.d/default.conf COPY dockerimage/start.sh /start.sh RUN chmod +x /start.sh -# Persistent data location -VOLUME ["/data"] - EXPOSE 80 ENV NODE_ENV=production diff --git a/dockerimage/README.md b/dockerimage/README.md index 6bb83bc..24382d9 100644 --- a/dockerimage/README.md +++ b/dockerimage/README.md @@ -1,19 +1,125 @@ -Self-hosted bundle -================== +Ciphernotes (Self‑Hosted) – Docker Compose +========================================= -Build and run everything with a single image plus Postgres and MinIO, persisting data to ./data. +Production‑ready, self‑hosted deployment of Ciphernotes. This setup runs: +- app: Nginx (serving the SPA and proxy) + backend (Bun) +- db: Postgres 16 +- minio: S3‑compatible object storage +- minio‑setup: one‑shot bucket initializer -Usage ------ +What you get +------------ +- Single `docker compose` up for the whole stack +- DB migrations auto‑applied on startup (no local tooling needed) +- MinIO bucket auto‑created +- Frontend served at port 8080; backend proxied at `/api`; S3 proxied at `/s3` with restricted methods -1) Copy `.env.example` to `.env` and adjust values. -2) Run: `docker compose -f dockerimage/docker-compose.yml up --build -d` -3) Open http://localhost:8080 - -Environment +Quick start ----------- +1) Copy env and adjust minimal settings: + - On first use: + - Create `dockerimage/.env` (or copy from an example) with at least: + - `COOKIE_SECRET` – strong random string + - Optional admin bootstrap (self‑hosted): `ADMIN_USERNAME`, `ADMIN_PASSWORD` + +2) Run: +``` +docker compose -f dockerimage/docker-compose.yml up --build -d +``` + +3) Open: +``` +http://localhost:8080 +``` + +Default ports +------------- +- App (Nginx + backend): 8080 → 80 +- MinIO API/Console (optional): 9000/9001 (published by default for convenience) + +Environment variables (app) +--------------------------- +- `COOKIE_SECRET` (required): secret for cookie signing +- `HOSTING_MODE` (optional): default `self` +- `ADMIN_USERNAME`, `ADMIN_PASSWORD` (optional): create/update an admin on first start +- Rate/limits (optional, sensible defaults): `RATE_LIMIT`, `RATE_WINDOW_SEC`, `SESSION_TTL_MIN`, `LIMIT_JSON`, `LIMIT_RAW`, `NOTES_STORAGE_LIMIT`, `FILES_STORAGE_LIMIT` + +Environment variables (db/minio) +-------------------------------- +- Postgres: `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` (defaults: `notes`/`notes`/`notes`) +- MinIO: `MINIO_ROOT_USER`, `MINIO_ROOT_PASSWORD` (defaults set in compose) +- S3 bucket: `S3_BUCKET` (default: `ciphernotes`) + +Security notes +-------------- +- The app proxies MinIO under `/s3` and limits methods to GET/HEAD/POST for browser flows. +- Keep MinIO root credentials secret. Anyone with valid credentials can perform actions through the proxy. +- Consider not publishing 9000/9001 externally in production; rely on the `/s3` proxy for browser access and the app’s internal S3 client for server‑side ops. + +Upgrades +-------- +- Pull new app image, then: +``` +docker compose -f dockerimage/docker-compose.yml pull app +docker compose -f dockerimage/docker-compose.yml up -d +``` +- Migrations run automatically on app startup. -- All persistent data is stored under `./data` on the host and mounted at `/data` in containers. -- The app runs in self-hosting mode with pro features for all users. +Troubleshooting +--------------- +- Check logs: +``` +docker compose -f dockerimage/docker-compose.yml logs -f app +``` +- Schema not created: ensure `db` is healthy, then restart `app`. +- S3 signature errors: ensure `/s3` proxy preserves `Host 127.0.0.1:9000` (already configured) and that the request path does not include the `/s3` prefix after proxying (rewrite is configured). +Docker Hub usage (publishing) +----------------------------- +If you publish the app image, your `compose.yml` can reference it directly, e.g.: +```yaml +services: + app: + image: your-dockerhub-username/ciphernotes-app:latest + env_file: [.env] + ports: ["8080:80"] + depends_on: + db: { condition: service_healthy } + minio: { condition: service_started } + minio-setup: { condition: service_completed_successfully } + db: + image: postgres:16 + environment: + POSTGRES_USER: ${POSTGRES_USER:-notes} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-notes} + POSTGRES_DB: ${POSTGRES_DB:-notes} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-notes}"] + interval: 5s + timeout: 5s + retries: 10 + start_period: 5s + volumes: ["db_data:/var/lib/postgresql/data"] + minio: + image: minio/minio:latest + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minio-admin} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minio-admin} + command: server /data --console-address ":9001" + ports: ["9000:9000", "9001:9001"] + volumes: ["minio_data:/data"] + minio-setup: + image: minio/mc:latest + depends_on: { minio: { condition: service_started } } + entrypoint: ["/bin/sh", "-c"] + command: >- + "mc alias set local http://minio:9000 ${MINIO_ROOT_USER:-minio-admin} ${MINIO_ROOT_PASSWORD:-minio-admin} && + (mc ls local/${S3_BUCKET:-ciphernotes} || mc mb -p local/${S3_BUCKET:-ciphernotes})" + restart: "no" +volumes: { db_data: {}, minio_data: {} } +``` +Licenses & attribution +---------------------- +- MinIO and `mc` are AGPLv3; in this setup you use the official images. See MinIO: https://github.com/minio/minio +- Ensure you comply with AGPLv3 when redistributing MinIO (e.g., include license notices and source references). diff --git a/dockerimage/docker-compose.yml b/dockerimage/docker-compose.yml index e3f35f5..e2dd8ed 100644 --- a/dockerimage/docker-compose.yml +++ b/dockerimage/docker-compose.yml @@ -1,4 +1,3 @@ -version: '3.9' services: app: build: @@ -8,10 +7,60 @@ services: env_file: - .env ports: - - "8080:80" + - "13064:80" + depends_on: + db: + condition: service_healthy + minio: + condition: service_started + minio-setup: + condition: service_completed_successfully + restart: unless-stopped + + db: + image: postgres:16 + container_name: ciphernotes_db + environment: + POSTGRES_USER: notes + POSTGRES_PASSWORD: notes + POSTGRES_DB: notes + healthcheck: + test: ["CMD-SHELL", "pg_isready -U notes"] + interval: 5s + timeout: 5s + retries: 10 + start_period: 5s volumes: - - ciphernotes_data:/data + - db_data:/var/lib/postgresql/data restart: unless-stopped + minio: + image: minio/minio:latest + container_name: ciphernotes_minio + env_file: + - .env + command: server /data --console-address ":9001" + ports: + - "9000:9000" + - "9001:9001" + volumes: + - minio_data:/data + restart: unless-stopped + + minio-setup: + image: minio/mc:latest + container_name: ciphernotes_minio_setup + env_file: + - .env + depends_on: + minio: + condition: service_started + entrypoint: ["/bin/sh", "-c"] + command: >- + "mc alias set local http://minio:9000 ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD} && + (mc ls local/ciphernotes || mc mb -p local/ciphernotes)" + restart: "no" + volumes: - ciphernotes_data: + db_data: + minio_data: diff --git a/dockerimage/start.sh b/dockerimage/start.sh index c163ed6..cfa3f03 100644 --- a/dockerimage/start.sh +++ b/dockerimage/start.sh @@ -1,99 +1,36 @@ #!/usr/bin/env bash set -euo pipefail -# Prepare data directories -mkdir -p /data/postgres /data/minio - ############################################ # Defaults for required backend env vars ############################################ export NODE_ENV=${NODE_ENV:-production} export HOSTING_MODE=${HOSTING_MODE:-self} export PORT=${PORT:-5100} -export TRUST_PROXY=${TRUST_PROXY:-1} +export TRUST_PROXY=${TRUST_PROXY:-3} export RATE_WINDOW_SEC=${RATE_WINDOW_SEC:-60} export RATE_LIMIT=${RATE_LIMIT:-200} export SESSION_TTL_MIN=${SESSION_TTL_MIN:-43200} export COOKIE_SECRET=${COOKIE_SECRET:-dev-cookie-secret} -export LIMIT_JSON=${LIMIT_JSON:-1mb} -export LIMIT_RAW=${LIMIT_RAW:-10mb} +export LIMIT_JSON=${LIMIT_JSON:-2mb} +export LIMIT_RAW=${LIMIT_RAW:-2mb} export NOTES_STORAGE_LIMIT=${NOTES_STORAGE_LIMIT:-1000000000} export FILES_STORAGE_LIMIT=${FILES_STORAGE_LIMIT:-1000000000} ############################################ -# Database URL default (internal Postgres) -############################################ -export DATABASE_URL=${DATABASE_URL:-postgresql://notes:notes@localhost:5432/notes} - -############################################ -# MinIO/S3 defaults (internal MinIO) +# External Postgres and MinIO endpoints via compose ############################################ -# If no password provided, read from file; if missing, generate and persist -MINIO_ROOT_PASSWORD_FILE=${MINIO_ROOT_PASSWORD_FILE:-/data/minio_root_password} -if [ -z "${MINIO_ROOT_PASSWORD:-}" ]; then - if [ -s "$MINIO_ROOT_PASSWORD_FILE" ]; then - MINIO_ROOT_PASSWORD="$(cat "$MINIO_ROOT_PASSWORD_FILE")" - else - MINIO_ROOT_PASSWORD="$(tr -dc 'A-Za-z0-9' "$MINIO_ROOT_PASSWORD_FILE" - chmod 600 "$MINIO_ROOT_PASSWORD_FILE" || true - fi -fi -export MINIO_ROOT_USER=${MINIO_ROOT_USER:-minio-admin} -export MINIO_ROOT_PASSWORD +export DATABASE_URL=${DATABASE_URL:-postgresql://notes:notes@db:5432/notes} +export MINIO_ROOT_USER=${MINIO_ROOT_USER} +export MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD} export S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID:-$MINIO_ROOT_USER} export S3_ACCESS_KEY_SECRET=${S3_ACCESS_KEY_SECRET:-$MINIO_ROOT_PASSWORD} export S3_REGION=${S3_REGION:-EU-CENTRAL-1} -export S3_ENDPOINT=${S3_ENDPOINT:-http://127.0.0.1:9000} +export S3_ENDPOINT=${S3_ENDPOINT:-http://minio:9000} export S3_BUCKET=${S3_BUCKET:-ciphernotes} ############################################ -# Start Postgres -############################################ -export PGDATA=/data/postgres -chown -R postgres:postgres "$PGDATA" -chmod 700 "$PGDATA" || true -if [ ! -s "$PGDATA/PG_VERSION" ]; then - echo "Initializing Postgres data directory..." - su -s /bin/sh postgres -c "/usr/lib/postgresql/*/bin/initdb -D $PGDATA -E UTF8 --no-locale" - # Relax auth for localhost - echo "local all all trust" >> "$PGDATA/pg_hba.conf" - echo "host all all 127.0.0.1/32 trust" >> "$PGDATA/pg_hba.conf" - echo "host all all ::1/128 trust" >> "$PGDATA/pg_hba.conf" -fi -# Start postgres directly bound to localhost -su -s /bin/sh postgres -c "/usr/lib/postgresql/*/bin/postgres -D $PGDATA -c listen_addresses=localhost" & -POSTGRES_PID=$! - -# Wait for Postgres up -until pg_isready -q -h 127.0.0.1 -p 5432; do echo "Waiting for Postgres..."; sleep 1; done - -# Ensure database exists (connect over TCP to avoid peer auth) -if ! su -s /bin/sh postgres -c "psql -h 127.0.0.1 -p 5432 -tc \"SELECT 1 FROM pg_roles WHERE rolname='notes'\"" | grep -q 1; then - su -s /bin/sh postgres -c "psql -h 127.0.0.1 -p 5432 -c \"CREATE ROLE notes LOGIN PASSWORD 'notes';\"" -fi -if ! su -s /bin/sh postgres -c "psql -h 127.0.0.1 -p 5432 -tc \"SELECT 1 FROM pg_database WHERE datname='notes'\"" | grep -q 1; then - su -s /bin/sh postgres -c "createdb -h 127.0.0.1 -p 5432 -O notes notes" -else - su -s /bin/sh postgres -c "psql -h 127.0.0.1 -p 5432 -c \"ALTER DATABASE notes OWNER TO notes;\"" || true -fi - -############################################ -# Start MinIO -############################################ -mkdir -p /data/minio -minio server /data/minio --console-address ":9001" & -MINIO_PID=$! - -# Wait for MinIO up -until curl -fsS http://127.0.0.1:9000/minio/health/ready >/dev/null 2>&1; do echo "Waiting for MinIO..."; sleep 1; done - -# Create bucket if not exists -mc alias set local http://127.0.0.1:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" -mc ls local/${S3_BUCKET} || mc mb -p local/${S3_BUCKET} - -############################################ -# Apply migrations only +# Apply migrations (DB health is ensured by compose depends_on) ############################################ cd /app/backend echo "Applying migrations..." @@ -108,12 +45,11 @@ BACKEND_PID=$! ############################################ # Start nginx (foreground) ############################################ -# Ensure no default site conflicts (keep our conf in conf.d) rm -f /etc/nginx/sites-enabled/default 2>/dev/null || true nginx -g 'daemon off;' & NGINX_PID=$! -wait -n $BACKEND_PID $NGINX_PID $MINIO_PID $POSTGRES_PID +wait -n $BACKEND_PID $NGINX_PID exit $? From 475e96c7b40dc5686b627f6de836fb4927ea03ce Mon Sep 17 00:00:00 2001 From: Raphael Nussbaumer Date: Wed, 3 Sep 2025 20:07:03 +0200 Subject: [PATCH 6/7] fix minio --- backend/src/endpoints/getPresignedUrls.ts | 10 ++-------- dockerimage/docker-compose.yml | 12 +++++++++--- dockerimage/nginx.conf | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/backend/src/endpoints/getPresignedUrls.ts b/backend/src/endpoints/getPresignedUrls.ts index 6bf3bbd..f54f8b2 100644 --- a/backend/src/endpoints/getPresignedUrls.ts +++ b/backend/src/endpoints/getPresignedUrls.ts @@ -116,10 +116,7 @@ const getUploadUrls = async ( }) return { note_id: note.clientside_id, - url: - hostingMode !== 'self' - ? url - : url.replace(/http:\/\/(localhost|127\.0\.0\.1|::1):9000/, '/s3'), + url: hostingMode !== 'self' ? url : url.replace(/http:\/\/[^\/]+\//, '/s3/'), fields, } }) @@ -154,10 +151,7 @@ const getDownloadUrls = async (note_ids: string[], user_id: number) => { ) return { note_id: note.clientside_id, - url: - hostingMode !== 'self' - ? url - : url.replace(/http:\/\/(localhost|127\.0\.0\.1|::1):9000/, '/s3'), + url: hostingMode !== 'self' ? url : url.replace(/http:\/\/[^\/]+\//, '/s3/'), } }) ) diff --git a/dockerimage/docker-compose.yml b/dockerimage/docker-compose.yml index e2dd8ed..f5186df 100644 --- a/dockerimage/docker-compose.yml +++ b/dockerimage/docker-compose.yml @@ -12,7 +12,7 @@ services: db: condition: service_healthy minio: - condition: service_started + condition: service_healthy minio-setup: condition: service_completed_successfully restart: unless-stopped @@ -40,6 +40,12 @@ services: env_file: - .env command: server /data --console-address ":9001" + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://localhost:9000/minio/health/ready || exit 1"] + interval: 5s + timeout: 5s + retries: 20 + start_period: 10s ports: - "9000:9000" - "9001:9001" @@ -54,11 +60,11 @@ services: - .env depends_on: minio: - condition: service_started + condition: service_healthy entrypoint: ["/bin/sh", "-c"] command: >- "mc alias set local http://minio:9000 ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD} && - (mc ls local/ciphernotes || mc mb -p local/ciphernotes)" + (mc ls local/ciphernotes || mc mb -p local/ciphernotes || true)" restart: "no" volumes: diff --git a/dockerimage/nginx.conf b/dockerimage/nginx.conf index c9bc996..9b42ae0 100644 --- a/dockerimage/nginx.conf +++ b/dockerimage/nginx.conf @@ -38,9 +38,9 @@ server { # Strip "/s3" prefix so MinIO sees "/ciphernotes/..." rewrite ^/s3/(.*)$ /$1 break; - proxy_pass http://127.0.0.1:9000; + proxy_pass http://minio:9000; proxy_http_version 1.1; - proxy_set_header Host 127.0.0.1:9000; # keep SigV4 host consistent + proxy_set_header Host minio:9000; # keep SigV4 host consistent 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; From 8afd87861752caf5f80fd63d07d638c346608913 Mon Sep 17 00:00:00 2001 From: Raphael Nussbaumer Date: Thu, 4 Sep 2025 07:13:08 +0200 Subject: [PATCH 7/7] github workflow --- .github/workflows/dockerhub-publish.yml | 65 +++++++++++++++++++++++++ .vscode/settings.json | 6 +++ 2 files changed, 71 insertions(+) create mode 100644 .github/workflows/dockerhub-publish.yml diff --git a/.github/workflows/dockerhub-publish.yml b/.github/workflows/dockerhub-publish.yml new file mode 100644 index 0000000..28fce40 --- /dev/null +++ b/.github/workflows/dockerhub-publish.yml @@ -0,0 +1,65 @@ +name: Build and publish Docker image to Docker Hub + +on: + push: + branches: [ main, master ] + tags: [ 'v*' ] + paths: + - 'dockerimage/**' + - 'backend/**' + - 'frontend/**' + - '.github/workflows/dockerhub-publish.yml' + pull_request: + branches: [ main, master ] + paths: + - 'dockerimage/**' + - 'backend/**' + - 'frontend/**' + - '.github/workflows/dockerhub-publish.yml' + +env: + DOCKERHUB_REPO: rapnuss/ciphernotes + +jobs: + docker: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.DOCKERHUB_REPO }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: dockerimage/Dockerfile + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64 + cache-from: type=gha + cache-to: type=gha,mode=max + + diff --git a/.vscode/settings.json b/.vscode/settings.json index a57879e..8a40482 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,6 +12,8 @@ "Betroffenenrechte", "bowser", "bubblewrap", + "buildx", + "Buildx", "CAROOT", "cascadia", "Cascadia", @@ -32,6 +34,7 @@ "datname", "dexie", "Dexie", + "dockerhub", "dockerimage", "downl", "DSGVO", @@ -39,6 +42,7 @@ "esnext", "Eurogamer", "genkeypair", + "github", "hcaptcha", "HCAPTCHA", "healthcheck", @@ -59,6 +63,7 @@ "levenshtein", "linecap", "linejoin", + "linux", "mantine", "matplotlib", "Metacritic", @@ -85,6 +90,7 @@ "pubout", "qrcode", "Qwant", + "rapnuss", "replit", "rgba", "rolname",