A web-based pastebin that renders Markdown content. Paste raw Markdown, get a shareable link that renders it beautifully.
Built with Next.js 16, React 19, SQLite, and server-side syntax highlighting via Shiki.
- GitHub Flavored Markdown (tables, task lists, strikethrough, footnotes)
- Syntax highlighting for code blocks (VS Code-quality, server-rendered)
- Light/dark theme
- Paste expiration (1 hour, 1 day, 1 week, 1 month, or never)
- Rate limiting
- XSS protection via rehype-sanitize and Content Security Policy headers
- Zero client-side JavaScript for paste viewing (Server Components)
- Marp presentations: pastes with a
marp: truefrontmatter render as interactive slide decks (keyboard + fullscreen navigation). Force a view with a URL suffix —.marpfor slides,.mdfor the document — without changing the core/idURL format. Link directly to a slide by appending#<n>(e.g./abc123#3opens on slide 3); the fragment updates as you navigate so the current slide is always shareable.
- Node.js 22+ (uses
better-sqlite3native module) - npm
git clone https://github.com/your-user/paste.md.git
cd paste.md
npm install
npm run devThe app starts at http://localhost:3000. The SQLite database is created automatically at data/paste.db.
A single script handles the full deployment — Node.js, build, systemd, nginx, firewall, and optional HTTPS:
# Public-facing server (attempts Let's Encrypt)
sudo bash setup.sh --domain paste.example.com
# Internal/corporate network (self-signed cert, no Let's Encrypt)
sudo bash setup.sh --internal --domain paste.internal.corp
# Internal with corporate CA certificate
sudo bash setup.sh --internal --ssl-cert /path/to/cert.crt --ssl-key /path/to/key.key
# Without a domain (self-signed, serves on the server's IP)
sudo bash setup.sh
# From a git repo instead of a local copy
sudo bash setup.sh --repo https://github.com/your-user/paste.md.git --domain paste.example.comFor step-by-step manual instructions, see below.
# Install Node.js 22
curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo bash -
sudo dnf install -y nodejs
# Build tools for better-sqlite3 native compilation
sudo dnf groupinstall -y "Development Tools"
sudo dnf install -y python3git clone https://github.com/your-user/paste.md.git /opt/paste-md
cd /opt/paste-md
npm ci --include=dev
npm run buildThe SQLite database lives at data/paste.db relative to the working directory. SQLite with WAL mode also creates paste.db-wal and paste.db-shm sidecar files, so the process needs write access to both the file and the directory:
mkdir -p /opt/paste-md/dataNote: Ownership is set in the next step. If you see
SQLITE_READONLYerrors, check that the service user owns thedata/directory and its contents — see Troubleshooting below.
sudo useradd --system --no-create-home --shell /sbin/nologin paste-md
sudo chown -R paste-md:paste-md /opt/paste-mdCreate /etc/systemd/system/paste-md.service:
[Unit]
Description=paste.md
After=network.target
[Service]
Type=simple
User=paste-md
Group=paste-md
WorkingDirectory=/opt/paste-md
ExecStart=/usr/bin/node node_modules/.bin/next start --port 3000
Restart=on-failure
RestartSec=5
# Environment
Environment=NODE_ENV=production
Environment=PORT=3000
# Hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/paste-md/data
PrivateTmp=true
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now paste-md
sudo systemctl status paste-mdsudo dnf install -y nginx
sudo systemctl enable --now nginx
# Allow nginx to proxy to Node.js (SELinux)
sudo setsebool -P httpd_can_network_connect 1
# Open firewall ports
# On Rocky Linux, firewalld may be masked by default — unmask and start it first
sudo systemctl unmask firewalld
sudo systemctl start firewalld
sudo systemctl enable firewalld
sudo firewall-cmd --permanent --add-service=http --add-service=https
sudo firewall-cmd --reloadCreate /etc/nginx/conf.d/paste-md.conf:
server {
listen 80;
server_name paste.example.com;
client_max_body_size 2M;
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
}sudo nginx -t
sudo systemctl reload nginxsudo dnf install -y epel-release
sudo dnf install -y certbot python3-certbot-nginx
sudo certbot --nginx -d paste.example.comCertbot sets up auto-renewal via a systemd timer.
Create Dockerfile in the project root:
FROM node:22-slim AS base
# Install build tools for better-sqlite3
RUN apt-get update && apt-get install -y build-essential python3 && rm -rf /var/lib/apt/lists/*
WORKDIR /app
FROM base AS deps
COPY package.json package-lock.json ./
RUN npm ci
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:22-slim AS runner
RUN apt-get update && apt-get install -y build-essential python3 && rm -rf /var/lib/apt/lists/*
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/package-lock.json ./package-lock.json
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public 2>/dev/null || true
COPY --from=builder /app/next.config.ts ./next.config.ts
COPY --from=builder /app/lib ./lib
COPY --from=builder /app/postcss.config.mjs ./postcss.config.mjs
RUN mkdir -p /app/data && chown nextjs:nodejs /app/data
USER nextjs
EXPOSE 3000
CMD ["node", "node_modules/.bin/next", "start", "--port", "3000"]Create .dockerignore:
node_modules
.next
data
.git
docker build -t paste-md .
docker run -d \
--name paste-md \
--restart unless-stopped \
-p 3000:3000 \
-v paste-md-data:/app/data \
paste-mdThe -v paste-md-data:/app/data flag persists the SQLite database across container restarts.
Create docker-compose.yml:
services:
paste-md:
build: .
restart: unless-stopped
ports:
- "3000:3000"
volumes:
- paste-md-data:/app/data
volumes:
paste-md-data:docker compose up -dThen set up nginx and HTTPS the same way as Option A (steps 6-7), pointing to 127.0.0.1:3000.
cd /opt/paste-md
git pull
npm ci --include=dev
npm run build
sudo systemctl restart paste-mddocker compose down
git pull
docker compose up -d --buildThe entire application state is a single SQLite file at data/paste.db. To back it up safely while the app is running (WAL mode is enabled):
sqlite3 data/paste.db ".backup '/path/to/backup/paste-$(date +%Y%m%d).db'"Or with a cron job:
0 3 * * * sqlite3 /opt/paste-md/data/paste.db ".backup '/backups/paste-$(date +\%Y\%m\%d).db'"SQLite in WAL mode needs write access to the data/ directory (not just the .db file) because it creates -wal and -shm sidecar files. This error means the Node.js process can't write to the database or its directory.
Check permissions:
ls -la /opt/paste-md/data/Fix:
sudo chown -R paste-md:paste-md /opt/paste-md/data
sudo chmod 755 /opt/paste-md/data
sudo chmod 644 /opt/paste-md/data/paste.db 2>/dev/null || true
sudo systemctl restart paste-mdIf using ProtectSystem=strict in the systemd unit (the default), ensure ReadWritePaths=/opt/paste-md/data is present in the service file.
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
Port the server listens on |
NODE_ENV |
development |
Set to production for deployments |
MIT