terminal://docs.self_hosting
[ DOCUMENTATION ]

Self-Hosting

Run Blurt on your own machine or server. Your posts, your infrastructure, zero dependencies on third-party services.

Prerequisites

  • Ruby 3.4+
  • libvips — for image processing (resize before upload)
  • SQLite3 — for Solid Queue job storage and PublishLog
# macOS
brew install ruby libvips sqlite3

# Ubuntu/Debian
sudo apt install ruby-full libvips-dev libsqlite3-dev

Installation

git clone https://github.com/fberrez/blurt.sh.git
cd blurt.sh
bundle install
cp .env.example .env
bin/rails db:prepare

Configuration

Edit .env with your credentials. You only need to configure the platforms you want to use.

API key

Required for the HTTP API. Generate a random secret:

openssl rand -hex 32
BLURT_API_KEY=your-generated-secret

Bluesky

Uses an app password (not your main password).

BLUESKY_SERVICE=https://bsky.social
BLUESKY_IDENTIFIER=your-handle.bsky.social
BLUESKY_PASSWORD=your-app-password

Mastodon

Generate an access token in Preferences > Development > New Application on your Mastodon instance.

MASTODON_URL=https://mastodon.social
MASTODON_ACCESS_TOKEN=your-access-token

LinkedIn

Requires an OAuth 2.0 token with w_member_social scope.

  • Create an app at LinkedIn Developers
  • Enable Share on LinkedIn and Sign In with LinkedIn using OpenID Connect
  • Add http://localhost:3847/callback as an authorized redirect URL
  • Run rake blurt:linkedin_auth to authenticate
LINKEDIN_CLIENT_ID=your-client-id
LINKEDIN_CLIENT_SECRET=your-client-secret
LINKEDIN_ACCESS_TOKEN=your-oauth-token
LINKEDIN_PERSON_ID=your-person-id

LinkedIn tokens expire every 60 days. Run rake blurt:linkedin_auth to re-authenticate when you get a 401 error.

Medium

Generate an integration token.

MEDIUM_INTEGRATION_TOKEN=your-integration-token

Dev.to

Generate an API key in Settings > Extensions.

DEVTO_API_KEY=your-api-key

Substack

Uses SMTP to email posts to your Substack import address. Posts arrive as drafts.

SUBSTACK_SMTP_HOST=smtp.gmail.com
SUBSTACK_SMTP_PORT=587
[email protected]
SUBSTACK_SMTP_PASSWORD=your-app-password
[email protected]
[email protected]

Running

Development

# Start everything (server + worker)
bin/dev

# Or start separately
bin/rails server
bin/jobs

The worker polls queue/ every 60 seconds. To change the interval:

POLL_INTERVAL_MS=30000  # 30 seconds

Verify it works

# Check health (no auth needed)
curl http://localhost:3000/api/health

# Check configured platforms
curl http://localhost:3000/api/platforms \
  -H "Authorization: Bearer $BLURT_API_KEY"

# Create and publish a test post
curl -X POST http://localhost:3000/api/posts \
  -H "Authorization: Bearer $BLURT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"platforms":["bluesky"],"content":"Hello from Blurt!"}'

curl -X POST http://localhost:3000/api/posts/hello-from-blurt.md/publish \
  -H "Authorization: Bearer $BLURT_API_KEY"

Docker

The fastest way to run Blurt in production. The included docker-compose.yml handles everything: image build, volume mounts, health checks, and auto-restart.

Quick start

git clone https://github.com/fberrez/blurt.sh.git
cd blurt.sh
cp .env.example .env
# Edit .env with your platform credentials

docker compose up -d

That's it. Drop markdown files in ./queue/ and Blurt publishes them. Published posts land in ./sent/ with permalinks in the frontmatter.

Volume mounts

Docker Compose bind-mounts three directories from the host so your posts stay on your machine:

Host Container Purpose
./queue/ /rails/queue/ Pending posts
./sent/ /rails/sent/ Published posts (with permalinks)
./failed/ /rails/failed/ Failed posts (with error details)

The container runs as UID 1000. Files in queue/ must be owned by this user. If you push files to a remote server as root (e.g. via scp), fix the ownership so Blurt can process them:

scp my-post.md root@your-server:/opt/blurt/queue/
ssh root@your-server "chown -R 1000:1000 /opt/blurt/queue/"

SQLite databases are stored in a Docker named volume (blurt_storage) and persist across container restarts.

Managing the container

# Check health
curl http://localhost/api/health

# View logs
docker compose logs -f

# Stop
docker compose down

# Rebuild after pulling updates
docker compose up -d --build

Environment variables

Platform credentials are loaded from .env at the project root. The container also requires RAILS_MASTER_KEY (from config/master.key) passed via the RAILS_MASTER_KEY environment variable or in .env.

To change the host port (default 80), set BLURT_PORT:

BLURT_PORT=8080 docker compose up -d

Environment reference

Variable Required Description
BLURT_API_KEY For API Bearer token for API authentication
POLL_INTERVAL_MS No Queue poll interval in ms (default: 60000)
BLUESKY_IDENTIFIER For Bluesky Your Bluesky handle
BLUESKY_PASSWORD For Bluesky App password
BLUESKY_SERVICE No AT Protocol service URL (default: https://bsky.social)
MASTODON_URL For Mastodon Instance URL
MASTODON_ACCESS_TOKEN For Mastodon Access token
LINKEDIN_ACCESS_TOKEN For LinkedIn OAuth 2.0 token
LINKEDIN_PERSON_ID For LinkedIn Your LinkedIn person URN
LINKEDIN_CLIENT_ID For re-auth OAuth client ID
LINKEDIN_CLIENT_SECRET For re-auth OAuth client secret
MEDIUM_INTEGRATION_TOKEN For Medium Integration token
DEVTO_API_KEY For Dev.to API key
SUBSTACK_SMTP_HOST For Substack SMTP server
SUBSTACK_SMTP_PORT No SMTP port (default: 587)
SUBSTACK_SMTP_USER For Substack SMTP username
SUBSTACK_SMTP_PASSWORD For Substack SMTP password
SUBSTACK_FROM_ADDRESS For Substack Sender email
SUBSTACK_TO_ADDRESS For Substack Substack import address

File structure

blurt.sh/
  queue/          # Drop markdown files here
  sent/           # Published posts (with permalinks)
  failed/         # Failed posts (with error details)
  .env            # Platform credentials
  storage/        # SQLite databases (Solid Queue, PublishLog)

The queue/, sent/, and failed/ directories are the only state that matters. Back them up, and you have a complete record of everything you've ever published.