Self-host with Docker Compose
Build from source and run migrations, web, worker, Postgres, and Redis as one portable stack.
This guide builds the current source checkout. Use an immutable release tag or commit for production; do not deploy a moving branch without recording the exact commit.
Prepare configuration
Clone the repository, copy the example environment, and replace every local-only credential before exposing the service:
git clone https://github.com/prosewire/prosewire.git
cd prosewire
cp .env.example .envAt minimum, set:
- A URL-safe, unique
POSTGRES_PASSWORD - A unique
BETTER_AUTH_SECRETof at least 32 characters - Matching external HTTPS origins in
PROSEWIRE_PUBLIC_URLandNEXT_PUBLIC_PROSEWIRE_PUBLIC_URL - A stable
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY SMTP_URLandEMAIL_FROMfor invitations- A deployment-specific
NEXT_DEPLOYMENT_IDfor the image build
Generate secrets with a cryptographically secure tool, for example:
openssl rand -hex 32
openssl rand -base64 32Read the configuration map before starting.
The bundled Compose build passes NEXT_DEPLOYMENT_ID into the web image. Change it when you build a new release, then run that same image on every web replica. Replacing the variable on an existing container does not change its built assets.
Understand the Compose topology
The default Compose file contains five services:
| Service | Purpose | Exposed publicly |
|---|---|---|
postgres | Persistent application database | No |
redis | Persistent Effect queue with AOF enabled | No |
migrate | One-shot committed schema migration | No |
web | Dashboard, APIs, embed, and public reader | Port 3000 |
worker | Scheduled publishing, analytics retention, and queued email delivery | No |
The web and worker wait for the migration service; the worker also waits for Redis. The web commits invitation email intents to Postgres. The worker turns those intents into durable Effect workflows, uses Redis for SMTP queue handoff, and stores workflow progress in Postgres. Run one worker container for this database. Production startup does not create the development seed, API key, publication, or sample posts. The migration job can create one temporary self-hosted administrator when explicitly configured.
Start and bootstrap
The recommended bootstrap does not open registration. Before the first start, add a unique email and temporary password to .env:
PROSEWIRE_BOOTSTRAP_ADMIN_EMAIL=owner@example.com
PROSEWIRE_BOOTSTRAP_ADMIN_PASSWORD=replace-with-a-unique-temporary-password
# PROSEWIRE_BOOTSTRAP_ADMIN_NAME=Prosewire AdminBoth credential variables must exist. The password must contain 12 to 128 characters. Do not reuse the database password or authentication secret.
docker compose up -d --build
docker compose ps
docker compose logs migrate redis web workerConfirm that migrate exited successfully and that the health endpoint responds:
curl --fail https://publish.example.com/api/healthThe migration job creates the administrator only when no user and no workspace exist. Sign in with the temporary password. Prosewire requires a new password immediately, revokes every existing session, and asks you to sign in again. The next screen creates the first publication and the installation’s implicit team.
Remove the bootstrap credential variables after changing the password, then recreate the long-running containers so the temporary secret no longer appears in their environment:
docker compose up -d --force-recreate web workerIf you prefer browser registration, leave the bootstrap credentials absent and set PROSEWIRE_ALLOW_SIGN_UP=true only for the first account. Turn it off and recreate the web container after onboarding. Once that account exists, direct self-hosted registration requires a valid, unexpired team invitation even if the flag remains enabled.
If the temporary password is lost before it is changed, replace PROSEWIRE_BOOTSTRAP_ADMIN_PASSWORD and run docker compose run --rm migrate. The migration job refreshes the credential only when it finds the sole matching bootstrap administrator, the password-change requirement is still active, and no workspace exists. After the password changes, the environment can never reset it. On an existing installation, bootstrap credentials are ignored rather than creating another administrator.
Upgrades do not delete or merge existing workspaces. If an older self-hosted database already contains more than one, members can still switch between them, but the instance cannot create another one. Move independent teams to separate installations before consolidating the data.
Upgrade safely
- Back up Postgres and record the running image or source commit.
- Read package and repository changelogs for the target version.
- Check out the intended immutable release tag or commit.
- Run
docker compose build, thendocker compose up -d. - Confirm the migration completed before web and worker become healthy.
- Verify
/api/health, sign-in, one published reader page, one authenticated export, worker logs, and a scheduled post.
Keep web, worker, and migrations on the same source version. Do not run different schema generations at the same time. If a migration is not backward compatible, rollback requires a tested database restore as well as the previous application image.
Migration 0013_friendly_slipstream.sql adds the required-password-change marker with a false default, so existing accounts are not forced through the bootstrap flow. If the migration job stops because bootstrap credentials are incomplete or invalid, correct or remove those variables and rerun it. The schema migration is idempotent. If the database itself cannot apply the column, keep the old release running and restore the pre-upgrade backup before retrying.
Migration 0011_flaky_bug.sql rejects content relationships that already cross publication boundaries. Check before upgrading:
select p.id as post_id, p.blog_id as post_blog_id,
a.id as author_id, a.blog_id as author_blog_id
from post p
join author a on a.id = p.author_id
where p.blog_id <> a.blog_id;
select pc.post_id, p.blog_id as post_blog_id,
pc.category_id, c.blog_id as category_blog_id
from post_category pc
join post p on p.id = pc.post_id
join category c on c.id = pc.category_id
where p.blog_id <> c.blog_id;Both queries must return no rows. If either returns data, keep the old release running, take a backup, and correct each post’s author or category to an ID from that post’s publication. Remove a join row only when the relationship itself is invalid. Rerun the one-shot migration after the queries are clean. The migration backfills post_category.blog_id, adds composite foreign keys, and leaves a compatibility trigger so an older process in a rolling replacement can still insert a valid category link without the new column.
Back up and restore
Back up Postgres with managed snapshots or pg_dump. Back up the Redis volume separately because delivery processing, retries, and failed email jobs live there after outbox dispatch. A Compose-hosted Postgres example is:
docker compose exec -T postgres pg_dump -U prosewire -d prosewire --format=custom > prosewire.dumpStore backups outside the application host, encrypt them when appropriate, and define a retention policy. Test restoration into a separate database and verify posts, authors, redirects, memberships, audit entries, exports, and public visibility before calling the backup usable.
The email_delivery_outbox table and Effect-owned cluster_* tables are safe to leave in place when rolling the application back, but an older web release does not create outbox intents. If Redis is lost and restored empty, stop the worker and restore Postgres and Redis to matching recovery points. Do not reset dispatched outbox rows by hand while their workflow execution remains suspended. Restore into a separate environment and verify one invitation workflow before replacing production data.
Portable JSON and CSV exports improve content portability but are not substitutes for a database backup because Prosewire does not currently provide a full import workflow.
Production checklist
- Terminate HTTPS at a trusted reverse proxy or load balancer
- Keep Postgres off the public internet and encrypt remote database connections
- Keep Redis on a private network, retain AOF persistence, and use
maxmemory-policy noeviction - Keep authentication, database, SMTP, API-key, and server-action secrets out of images and source control
- Configure SMTP and test an invitation before onboarding the team
- Keep
PROSEWIRE_ALLOW_SIGN_UP=falseafter bootstrap - Remove
PROSEWIRE_BOOTSTRAP_ADMIN_EMAILandPROSEWIRE_BOOTSTRAP_ADMIN_PASSWORDafter the first password change - Pin an immutable source commit or image digest instead of
latest - Run exactly one supervised worker for each database and use
PROSEWIRE_EMAIL_WORKER_CONCURRENCYfor parallel SMTP delivery - Monitor migration exit status, web health, worker failures, Redis queue age, disk usage, and database capacity
- Verify a database restore and content export before launch