A minimal Markdown paste service. Paste, save, share — rendered client-side with a CRT/terminal aesthetic.
Live: md.gimeno.dev
docker compose up --buildOpen http://localhost:20080.
Create a server/.env with your PostgreSQL connection (this file is gitignored):
PORT=20080
HOST=0.0.0.0
DATABASE_URL=postgresql://user:pass@host:5432/mdshare
MAX_CONTENT=1000000The pastes table is created automatically on startup.
To run a fully self-contained stack with a local PostgreSQL, use the db profile:
DATABASE_URL=postgresql://mdshare:mdshare@db:5432/mdshare # in server/.env
docker compose --profile db up --buildThe Dockerfile is a multi-stage production build — it compiles the React client and serves it from Express with no dev dependencies:
docker build -t mdshare .
docker run -p 20080:20080 -e DATABASE_URL=postgresql://... mdsharePrebuilt images are published to GHCR on every version tag:
docker run -p 20080:20080 -e DATABASE_URL=postgresql://... ghcr.io/godgmn/mdshare:latest| Variable | Required | Default | Purpose |
|---|---|---|---|
DATABASE_URL |
Yes | — | PostgreSQL connection string |
PORT |
No | 20080 |
HTTP port |
HOST |
No | 0.0.0.0 |
Bind address |
MAX_CONTENT |
No | 1000000 |
Max paste size (bytes) |
PASTE_TTL_DAYS |
No | 30 |
Pastes older than this are deleted hourly; 0 disables expiry |
TRUST_PROXY |
No | — | Set when running behind a reverse proxy so per-IP rate limiting works. Use the number of proxy hops (e.g. 1) or a proxy subnet; true is rejected |
Anonymous pastes are rate limited per IP to 10 posts per minute; reads are limited to 60 per minute (both in-memory; they reset on restart). Empty or whitespace-only pastes are rejected with a 400. PORT, MAX_CONTENT, PASTE_TTL_DAYS, and TRUST_PROXY are validated at startup — the server exits with a clear error if they are not valid. Released images are multi-platform (linux/amd64, linux/arm64), scanned with Trivy, smoke-tested against a real PostgreSQL, signed with cosign, and published with an SPDX SBOM.
If mdshare runs behind nginx/Traefik/Caddy, set TRUST_PROXY=1 (or your proxy count) or a trusted subnet. Without it, the rate limiter sees the proxy's IP for every client and throttles all users together. TRUST_PROXY=true is rejected because it trusts every address in the X-Forwarded-For chain, letting clients spoof their IP and evade the rate limits.
make lint # ESLint (server + client)
make test # server tests + client tests
make up # docker composeServer integration tests run against a real PostgreSQL database when TEST_DATABASE_URL is set; without it they are skipped:
cd server && TEST_DATABASE_URL=postgresql://user:pass@host:5432/mdshare_test npm test- Backend: Node.js, Express, PostgreSQL (
pg) - Frontend: React, Vite, markdown-it, DOMPurify
- Fonts: IBM Plex Mono, VT323 (self-hosted)
MIT © Gimeno
Contributing: see CONTRIBUTING.md. Security: see SECURITY.md.
