ProsewireDocs

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 .env

At minimum, set:

  • A URL-safe, unique POSTGRES_PASSWORD
  • A unique BETTER_AUTH_SECRET of at least 32 characters
  • Matching external HTTPS origins in PROSEWIRE_PUBLIC_URL and NEXT_PUBLIC_PROSEWIRE_PUBLIC_URL
  • A stable NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
  • SMTP_URL and EMAIL_FROM for invitations
  • A deployment-specific NEXT_DEPLOYMENT_ID for the image build
  • S3-compatible PROSEWIRE_MEDIA_* settings if editors need managed uploads

Generate secrets with a cryptographically secure tool, for example:

openssl rand -hex 32
openssl rand -base64 32

Read 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:

ServicePurposeExposed publicly
postgresPersistent application databaseNo
redisPersistent Effect queue with AOF enabledNo
migrateOne-shot committed schema migrationNo
webDashboard, APIs, embed, and public readerPort 3000
workerScheduled publishing, analytics retention, and queued email deliveryNo

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 Admin

Both 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 worker

Confirm that migrate exited successfully and that the health endpoint responds:

curl --fail https://publish.example.com/api/health

The 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 worker

If 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.

Verify the first publication

After creating the first publication, publish one article and open its reader from the dashboard. Create a draft and a future scheduled post, and verify they do not appear in the public index or public JSON endpoint. Archive a published post and confirm its public URL returns 404. Rename the remaining published article and confirm the previous slug redirects to the replacement.

For an embedded Next.js or Astro reader, follow the Next.js guide or Astro guide using this installation’s external URL and publication slug. Build the host application and repeat the visibility checks. Static Astro output requires rebuilding and redeploying after publishing, archiving, or renaming.

Record the exact checkout and the running image IDs before calling the installation verified:

git rev-parse HEAD
docker compose images
curl --fail https://publish.example.com/api/health

The health response proves the web process can reach its configured database. Verify migration exit status and worker logs separately; it does not prove either process is running correctly.

Upgrade safely

  1. Back up Postgres and record the running image or source commit.
  2. Read package and repository changelogs for the target version.
  3. Check out the intended immutable release tag or commit.
  4. Run docker compose build, then docker compose up -d.
  5. Confirm the migration completed before web and worker become healthy.
  6. 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 0015_freezing_norman_osborn.sql adapts accounts to Better Auth 1.7.5. It retains historical issuer values, makes the field optional, and replaces issuer-based uniqueness with (provider_id, account_id) uniqueness. Before upgrading, stop authentication writes, back up Postgres, and check for collisions:

SELECT provider_id, account_id, count(*)
FROM account
GROUP BY provider_id, account_id
HAVING count(*) > 1;

The query must return no rows. If it finds collisions, keep the old release running and review the account owners and provider configuration. Do not merge different users or delete their accounts automatically. The migration creates the new unique index first and refuses collisions before relaxing the old constraints. Resolve each identity using trusted provider records, then rerun the migration. Deploy web and worker together after migration. To return to Better Auth 1.7.0 through 1.7.2 after accepting new accounts, restore the pre-upgrade database backup and previous application together; new accounts can lack an issuer, so reverting the application alone is unsafe.

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. If media storage is configured, snapshot or replicate the media bucket and record the same recovery point as the database backup. A Compose-hosted Postgres example is:

docker compose exec -T postgres pg_dump -U prosewire -d prosewire --format=custom > prosewire.dump

Store backups outside the application host, encrypt them when appropriate, and define a retention policy. Test restoration into a separate database and object-storage namespace. Verify posts, authors, redirects, memberships, audit entries, media checksums, 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, CSV, and media ZIP exports improve content portability but are not substitutes for coordinated database and object-storage backups because Prosewire does not currently provide a full import workflow.

Migration 0014_yummy_wildside.sql only adds tables, columns, indexes, and constraints. Existing external cover URLs remain unchanged and their managed-asset identifier stays null. Before rollback, stop writes and take a database backup. An older application ignores the new media tables and column, but cannot understand managed cover references. Restore the database and media bucket together if the migration or upload rollout must be reversed. Do not drop the new tables until every post has cover_image_asset_id cleared and a replacement external URL has been verified.

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 the media bucket private, restrict CDN paths to processed objects, and test signed upload CORS from the dashboard origin
  • Keep PROSEWIRE_ALLOW_SIGN_UP=false after bootstrap
  • Remove PROSEWIRE_BOOTSTRAP_ADMIN_EMAIL and PROSEWIRE_BOOTSTRAP_ADMIN_PASSWORD after 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_CONCURRENCY for parallel SMTP delivery
  • Monitor migration exit status, web health, worker failures, Redis queue age, disk usage, and database capacity
  • Verify a coordinated database, Redis, and media restore plus every export format before launch