Zer0 is a small multi-user fediverse microblog built with Next.js. User posts are called zosts; in ActivityPub they are represented mainly as Note objects.
Current functionality includes email/password accounts, invite-based registration after the first admin user, local profiles, posting with visibility controls, replies, likes, announces, bookmarks, image uploads, notifications, basic admin tools, and ActivityPub federation support.
Zer0 runs as two processes:
- the Next.js web app
- a worker process for queues, federation delivery, media processing, and timeline fanout
It requires Postgres and Redis.
Build or pull the Docker image, run migrations, and start the web and worker processes:
APP_ORIGIN=https://your-domain.example \
AUTH_SECRET=change-this-to-a-long-random-secret \
docker compose up -d --buildBy default, the production compose file starts Postgres, Redis, the Next.js web app, a one-shot migration service, and the worker process. The web app listens on port 3000; set ZER0_PORT to publish a different host port.
The compose file uses its bundled Postgres and Redis services by default. To use managed services instead, set:
ZER0_DATABASE_URL=postgres://user:password@host:5432/zer0
ZER0_REDIS_URL=redis://host:6379To use an image built by GitHub Actions instead of building locally, set ZER0_IMAGE:
ZER0_IMAGE=ghcr.io/uvexz/zer0:latest docker compose up -dThe Docker workflow publishes linux/amd64 and linux/arm64 images to ghcr.io/<owner>/<repo> on pushes to main and version tags.
For production media storage, prefer S3-compatible storage:
MEDIA_STORAGE_DRIVER=s3
MEDIA_S3_BUCKET=...
MEDIA_S3_REGION=...
MEDIA_S3_ENDPOINT=...
MEDIA_S3_ACCESS_KEY_ID=...
MEDIA_S3_SECRET_ACCESS_KEY=...
MEDIA_S3_PUBLIC_BASE_URL=...If MEDIA_STORAGE_DRIVER=local, the compose file stores media in the media_data Docker volume shared by the web and worker containers.
Use the same image or repository as two separate long-running services:
web: bun run start
worker: bun run worker
Run migrations once per deploy:
bun run db:migrateCopy the example environment file:
cp .env.example .envSet at least:
APP_ORIGIN=https://your-domain.example
AUTH_SECRET=change-this-to-a-long-random-secret
DATABASE_URL=postgres://user:password@host:5432/zer0
REDIS_URL=redis://host:6379For a small managed Postgres plan, set DATABASE_MAX_CONNECTIONS=4 or lower. This limit is per process, so the web app and worker each get their own pool.
For local file storage:
MEDIA_STORAGE_DRIVER=local
MEDIA_LOCAL_DIR=.localdata/mediaFor S3-compatible storage, set MEDIA_STORAGE_DRIVER=s3 and fill the MEDIA_S3_* variables in .env.example.
bun install
bun run db:migrate
bun run seedThe seed command creates the invite code ZER0-LOCAL unless SEED_INVITE_CODE is set.
For local testing, Postgres and Redis can be started with:
docker compose -f docker-compose.dev.yml up -d postgres redisThen run the web app and worker as local processes:
bun run dev
bun run workerTo test a production build locally without Docker:
bun run build
bun run startOpen the configured APP_ORIGIN in a browser. The first registered account does not need an invite and becomes the admin account. Later accounts require an invite code.
curl https://your-domain.example/api/healthA healthy deployment returns:
{"postgres":true,"redis":true}