Migrating phpBB to Discourse — The Real Walkthrough (2026)

Migrating to Discourse is not a one-click affair. The import scripts live inside the Discourse Docker container, the source MySQL has to be reachable from that container, attachments need a path-mapping dance, usernames break on characters Discourse refuses, and once the import finishes you stare at a Sidekiq queue with hundreds of thousands of jobs that have to drain before users can log in. This guide is the end-to-end phpBB → Discourse run, with vBulletin notes at the bottom.

discourse.example.com/admin/migration
SimpleReview extension
Discourse · Importing from phpBB 3.3
Users12,481 / 12,481 ✓
Topics48,202 / 48,202 ✓
Posts421,930 / 491,612
Attachments0 / 8,940 — path missing
[INFO] importing posts batch 422 of 492
[INFO] sidekiq queue depth: 318,402
[WARN] attachment file not found: files/2_a3f9...
[WARN] ATTACHMENTS_BASE_DIR may be wrong
LatestCategoriesTop
✓ Import complete · 8,940 attachments restored · sidekiq drained
Welcome to the new forum
Comment×
phpBB attachments missing — fix path mapping?|
Fix it ✓ Done
waiting for selection…
Detected
Issueattach 0/8940
VarATTACH_BASE
Fix plan
Mount /phpbb/files into container · set ATTACHMENTS_BASE_DIR · re-run attach phase only
Result
8,940 attachments imported · sidekiq draining 318k → 0
✓ Fix ready
fix(import): mount phpBB files dir
containers/app.yml · 4 lines
Click SimpleReview → select the failing attachment row → Fix it → ATTACHMENTS_BASE_DIR + volume mount patched
Stuck on a half-imported forum with broken attachments or a Sidekiq queue that won't drain? → SimpleReview reads your containers/app.yml, the import log, and the Sidekiq stats, then prepares a fix with the right ATTACHMENTS_BASE_DIR, volume mount, and UNICORN_SIDEKIQS bump — faster than re-running a 12-hour import.

Key Takeaways (TL;DR)

  • The script lives in the Discourse repo at script/import_scripts/phpbb3.rb — and it must run inside the Discourse Docker container, not on the host.
  • Prerequisites: phpBB 3.x (upgrade 2.x first), MySQL reachable from container, attachments dir mounted in, fresh Discourse install (or IMPORT_DESTRUCTIVE=1).
  • Always dry-run first — fork the script with BATCH_SIZE=100 and a hard-coded LIMIT to validate connectivity, BBCode conversion, and username remap before committing 12 hours of CPU.
  • Attachments are the #1 footgun: phpBB's files/ directory has to be mounted into the container at the path ATTACHMENTS_BASE_DIR points to.
  • Sidekiq drain is the second half of the migration. Watch Sidekiq::Stats.new.enqueued in a Rails console; bump UNICORN_SIDEKIQS=4 in app.yml to drain faster.

What Actually Gets Imported

Before you start, set expectations. Discourse's importer is not a 1:1 mirror — it's an opinionated remapping into Discourse's data model. Some things round-trip cleanly, others lose fidelity, others vanish entirely.

phpBB conceptMaps to (Discourse)Fidelity
Users (with bcrypt/phpBB3 hashes)Users; password reset on first login✓ Full — incl. signature, avatar, registration date
Forums / sub-forumsCategories / sub-categories✓ Full — permissions reset, you re-grant
TopicsTopics✓ Full — pinned, locked flags preserved
Posts (BBCode)Posts (Markdown + HTML)~ Mostly — standard tags convert; obscure mod tags survive as plain text
Attachments & inline imagesUploadsOnly if ATTACHMENTS_BASE_DIR is set right
Private messages (1-to-1)Personal messages~ Mostly — group PMs sometimes collapse to threads
Topic subscriptions / "watching"✗ Not imported — users re-watch
Reactions / +1 / phpBB karma✗ Not imported (phpBB has no native concept)
Custom profile fields (mod-defined)✗ Not imported unless you fork the script
Ranks & badges✗ Re-create with Discourse Trust Levels & badges
Custom BBCode (e.g. [highlight])✗ Survives as raw text; needs a discourse remap pass

For vBulletin migrations the table is essentially the same except reactions and thanks are imported (vBulletin does have those natively, and the importer respects them) and the BBCode loss is bigger because vBulletin lets admins register arbitrary tags.

Prerequisites — What to Set Up Before You Touch the Script

1. Source side: phpBB 3.x

The phpbb3.rb importer only understands phpBB 3.x schema (3.0, 3.1, 3.2, 3.3 all work — 3.3 is the smoothest). If you're on phpBB 2.x, upgrade first using phpBB's own UMIL/converter to at least 3.0, then ideally to 3.3, then point the importer at the upgraded MySQL. Going directly from 2.x is not supported.

You need read access to the phpBB MySQL database — the script never writes to phpBB. Either:

2. Disk for attachments

Estimate: phpBB attachments live in files/ at the forum root. Run du -sh files/ on the source — that's your floor. Discourse re-encodes images and generates thumbnails, so reserve 2-3x that figure on the Discourse host. A 20 GB attachment dir typically needs 50-60 GB of free disk after import.

3. Target side: a fresh Discourse

The official guidance is unambiguous: import into a fresh Discourse. The importer assumes user IDs, post IDs and category IDs that it can allocate freely. Importing into an existing forum with real users in it will collide on user IDs, mangle topics, and generally ruin your day.

If you must import into a non-empty Discourse (e.g. a staff Discourse that already exists), set:

IMPORT_DESTRUCTIVE=1 bundle exec ruby script/import_scripts/phpbb3.rb

This wipes the Discourse database before importing. There is no in-place "merge" mode. If you want to truly merge two forums, run two imports into two fresh Discourses, then export one and re-import via the Discourse-to-Discourse importer.

4. Network: container ↔ source DB

The import script runs inside the app Docker container. From inside that container, you must be able to mysql -h SOURCE_HOST -u user -p phpbb_db. Common pitfalls:

Step-by-Step: phpBB → Discourse

  1. Spin up Discourse normally. SSH into the target server, install Docker, clone discourse/discourse_docker into /var/discourse, configure containers/app.yml (SMTP, hostname, admin email), and run ./discourse-setup or ./launcher rebuild app. Confirm the empty Discourse loads at your domain. Do not create real users yet.
  2. Mount the phpBB attachments directory into the container. Edit containers/app.yml and add a volume in the volumes: block:
    volumes:
      - volume:
          host: /var/discourse/shared/standalone
          guest: /shared
      - volume:
          host: /srv/phpbb-files
          guest: /shared/import/phpbb-files
    Copy or rsync the phpBB files/ directory to /srv/phpbb-files on the host. Rebuild: ./launcher rebuild app.
  3. Enter the container.
    cd /var/discourse
    ./launcher enter app
    Everything below runs inside that shell.
  4. Configure the importer. The phpBB importer reads a YAML at script/import_scripts/phpbb3/settings.yml. Copy the example:
    cd /var/www/discourse
    cp script/import_scripts/phpbb3/settings.yml \
       script/import_scripts/phpbb3/settings.local.yml
    vim script/import_scripts/phpbb3/settings.local.yml
    Set database, username, password, host, table_prefix (usually phpbb_), and the absolute container path to attachments under phpbb_base_dir (e.g. /shared/import/phpbb-files).
  5. Dry-run on a sample. Don't commit the full import yet. Edit the script (or use the built-in POSTS_LIMIT/SKIP_* envs depending on the script version) to import only the first 100 topics. Verify usernames look right, BBCode rendered correctly, attachments resolve.
  6. Run the real import.
    cd /var/www/discourse
    bundle exec ruby script/import_scripts/phpbb3.rb \
      script/import_scripts/phpbb3/settings.local.yml \
      2>&1 | tee /shared/import.log
    Tail the log in another shell (./launcher enter app in a second window, then tail -f /shared/import.log). Expect: hours, not minutes. A 500k-post forum on a 4-vCPU box took ~9 hours in our test run.
  7. Username fixup. phpBB allows usernames Discourse refuses (spaces, %, leading digits, etc.). The importer auto-rewrites them, but the rewrite isn't always pretty. Run a quick audit in a Rails console: User.where("username LIKE ?", "%\\_%").count. Use UserUpdater to bulk-rename if needed (see the script below).
  8. Wait for Sidekiq to drain. The hardest, least-glamorous step. Section below covers monitoring and tuning.

Environment Variables and Settings That Matter

Different versions of the script read settings from different places. Modern Discourse (post-2018) uses the YAML; older versions used env vars. Here's the canonical set you'll be touching:

SettingWhat it doesTypical value
databasephpBB MySQL DB namephpbb
username / passwordMySQL credentialsRead-only user
hostMySQL host as seen from container172.17.0.1 or db.internal
table_prefixphpBB tables prefixphpbb_
phpbb_base_dirContainer path to phpBB files//shared/import/phpbb-files
username_as_emailIf a user has no email, fabricate onetrue
site_nameUsed in email-fabrication domainexample.com
BATCH_SIZE (env)Rows fetched per query1000 default; lower if OOM
IMPORT_DESTRUCTIVE (env)Wipe Discourse DB first1 if forum not fresh
RAILS_ENVAlways production inside containerproduction
Do not run database migrations on an existing production Discourse before importing. Some old guides tell you to run RAILS_ENV=production bundle exec rake db:migrate before the import. That's already done by ./launcher rebuild; running it again does nothing useful and on certain Discourse versions has side effects.

The Attachment Problem (and Its Fix)

This is the single most common point of failure. Symptoms:

The mechanic: phpBB stores attachments by hash filename in files/, with the database row referencing the hash. The importer reads the row, computes {phpbb_base_dir}/{physical_filename}, and tries to File.read it. If the path is wrong, it silently logs a warning and continues.

Fix it

  1. From inside the container: ls /shared/import/phpbb-files | head. You should see hash-style filenames like 2_a3f9b2c0c2c3a1b4. If you see an empty directory, your volume mount is wrong — re-check containers/app.yml.
  2. If the directory has files but the importer still warns, the phpbb_base_dir setting is pointing somewhere else. Match it byte-for-byte to the container path you see in ls.
  3. If files are there but renamed (some shared hosts strip the 2_ prefix), you need to fork the script's find_filename method. This is rare — the default works on stock phpBB.

If you finished the import and only realized attachments are broken later, you don't have to redo everything. Re-run the importer; it skips anything already imported and retries failed attachments.

Username Fixup

phpBB allows usernames like John Doe, 50%off, and even Cyrillic. Discourse's username regex is much stricter (alphanumeric, ., _, -; first char alphanumeric; ASCII only by default). The importer remaps automatically — usually by replacing illegal chars with _ — but the result can be ugly or collide.

After import, audit:

# inside container, in Rails console
./launcher enter app
cd /var/www/discourse
RAILS_ENV=production bundle exec rails console

> User.where("username ~ '_{2,}'").count
> User.where("username LIKE '%\\_\\_%'").pluck(:id, :username).take(20)

For a clean rewrite of the worst offenders:

# Rename users with double-underscore artifacts
User.where("username LIKE '%\\_\\_%'").find_each do |u|
  new = u.username.gsub(/_{2,}/, '_').sub(/^_+/, '').sub(/_+$/, '')
  next if new.blank? || User.exists?(username_lower: new.downcase)
  UsernameChanger.change(u, new, u)
end

If you want to enable Unicode usernames globally first (so the importer stops mangling Cyrillic / CJK names), set unicode_usernames in admin → settings before importing.

Post-Import: Sidekiq Drain

The import script writes raw rows. It does not bake posts (cook Markdown to HTML), build the search index, or generate image thumbnails. All of that happens via background Sidekiq jobs queued during import. On a 500k-post forum that's typically 300-700k jobs queued.

Check queue depth:

# inside Rails console
> Sidekiq::Stats.new.enqueued
=> 318402

> Sidekiq::Stats.new.queues
=> {"default"=>284091, "low"=>34211, "ultra_low"=>100}

Or visit /sidekiq as an admin in the browser — Discourse exposes the Sidekiq web UI.

Tuning the drain

Default Discourse runs one Sidekiq process per Unicorn master. To drain faster, bump it temporarily in containers/app.yml:

env:
  UNICORN_SIDEKIQS: 4
  # also bump if memory allows
  DISCOURSE_DB_POOL: 25

Rebuild: ./launcher rebuild app. Keep an eye on RAM — each extra Sidekiq process is ~300 MB resident. On an 8 GB box don't go above 4. Once the queue is empty, set UNICORN_SIDEKIQS back to the default (1) and rebuild again.

Common slow jobs

Rebake and Remap (the cleanup pass)

Once Sidekiq is empty, run a forced rebake to catch posts that didn't process correctly:

# inside Rails console
> Post.where("created_at > ?", 1.day.ago).find_each(&:rebake!)

Or via rake (faster for full forums):

RAILS_ENV=production bundle exec rake posts:rebake

For URL fixups — old phpBB viewtopic.php?t=123 links scattered through post bodies — use discourse remap:

./launcher enter app
cd /var/www/discourse

# preview first
RAILS_ENV=production bundle exec script/discourse remap \
  "oldforum.com/viewtopic.php?t=" "discourse.example.com/t/" --dry-run

# do it
RAILS_ENV=production bundle exec script/discourse remap \
  "oldforum.com/viewtopic.php?t=" "discourse.example.com/t/"

# regex variant
RAILS_ENV=production bundle exec script/discourse remap \
  --regex "(http://oldforum\\.com)" "https://discourse.example.com"

Subdomain → Root Migration: 301 Map

If your old phpBB lived at forum.example.com and Discourse now lives there too (or vice-versa), you need 301s for the old phpBB URL patterns. The script logs every topic_id → discourse_topic_id mapping in the import database tables; you can dump it and feed it to nginx.

Minimal nginx redirect block on the old host (or in front of Discourse if you kept the same domain):

# /etc/nginx/sites-available/phpbb-redirect.conf
location /viewforum.php {
  if ($arg_f) {
    return 301 https://forum.example.com/c/$arg_f;
  }
}
location /viewtopic.php {
  if ($arg_t) {
    return 301 https://forum.example.com/t/$arg_t;
  }
}
location = /memberlist.php { return 301 https://forum.example.com/u; }
location = /index.php       { return 301 https://forum.example.com/; }

Discourse will resolve /t/<id> to the real topic slug and 301 again to the canonical URL. Two hops — but Google handles it fine and your link equity survives.

Validation Checklist (Before You Open the Doors)

  1. Row counts match.
    # source MySQL
    SELECT COUNT(*) FROM phpbb_users WHERE user_type != 1;
    SELECT COUNT(*) FROM phpbb_topics;
    SELECT COUNT(*) FROM phpbb_posts;
    
    # target Postgres (Rails console)
    > User.real.count
    > Topic.count
    > Post.count
    Discourse counts will be slightly higher (system user, welcome topic). Anything more than +5 is suspicious.
  2. Spot-check 5 random topics. Pick the most-viewed phpBB topics; open them in Discourse; check formatting, attachments, quoted replies.
  3. Test a user login. Pick a user with a known email; do a "Forgot password" flow; confirm reset email arrives and login works.
  4. Search works. Search for a phrase you know exists. If results are empty, the search index hasn't built yet — go back to Sidekiq drain.
  5. Categories and permissions. Default is "everyone can read"; if your phpBB had private forums, re-create those permissions in Discourse before users log in.
  6. Attachments load. Open a post you know has an image; check the image renders, not a broken-thumbnail icon.
  7. Email is sane. Send a test email from admin → email; verify it arrives. Discourse will fan out lots of email after launch — make sure SPF/DKIM are correct first.

Notes for vBulletin → Discourse

The pattern is identical:

Reality check: A "1-million-post vBulletin → Discourse" migration is not a weekend project. Plan a 2-week window: 2 days planning, 1 day staging import, 3-4 days fixing custom-BBCode/attachment edge cases on staging, 1 day production import, 2-3 days Sidekiq drain + rebake, 1 day comms / DNS cutover, several days hand-holding users post-launch.

Common Errors and Fixes

ErrorCauseFix
Mysql2::Error: Can't connect to MySQL serverContainer can't reach source DBUse Docker bridge IP 172.17.0.1, not localhost
Mysql2::Error: Access denied for user in MySQL 8caching_sha2_passwordRecreate user with IDENTIFIED WITH mysql_native_password
WARN attachment file not foundphpbb_base_dir wrong or volume not mountedSee "Attachment Problem" section
ActiveRecord::RecordNotUnique on usernameTwo phpBB users normalize to same Discourse usernamePre-rename one in phpBB before import
NoMethodError: undefined method `cook'Running script outside container / wrong RAILS_ENVAlways ./launcher enter app, always production
Sidekiq queue stops drainingWorker crashedSidekiq::ProcessSet.new.size in console; ./launcher restart app
Out of memory during importBATCH_SIZE too highSet BATCH_SIZE=200, retry
Some posts show raw BBCodeCustom phpBB mod tagPost-import discourse remap --regex

When to Ask Humans (or AI) for Help

Discourse migrations have an unfortunate property: most failure modes are silent. The script prints "Done" with broken attachments and you only notice three days later when a user complains. SimpleReview can:

Useful when you don't want to learn Sidekiq internals on your launch day.

Don't Re-Run the 12-Hour Import — Patch the Config Instead

Click on the failing import row, type the symptom, hit Fix it. SimpleReview reads your app.yml and import log, prepares a fix with the corrected mount path, env var, or Sidekiq tuning.

Install SimpleReview Chrome Extension →

Running Discourse on your own infra? SimpleReview also reads server logs over SSH →

Frequently Asked Questions

How do I migrate from phpBB to Discourse?
Use the official script/import_scripts/phpbb3.rb inside the Discourse Docker container. Mount phpBB's files/ directory into the container, give the container network access to phpBB's MySQL, configure script/import_scripts/phpbb3/settings.local.yml with DB credentials and phpbb_base_dir, then run bundle exec ruby script/import_scripts/phpbb3.rb settings.local.yml. Wait for Sidekiq to drain afterward.
Where do the Discourse import scripts live?
Inside the Discourse repo at script/import_scripts/. phpbb3.rb for phpBB 3.x, vbulletin.rb for vB 3/4, vbulletin5.rb for vB 5, plus vanilla.rb, smf2.rb, mybb.rb, ipboard.rb, etc. They must run inside the Discourse container (./launcher enter app), not on the host.
What gets imported from phpBB?
Users (passwords reset on first login), forums → categories, topics, posts (with most BBCode converted), attachments and inline images, and 1-to-1 private messages. Not imported: subscription/watching state, custom user titles from non-standard mods, ranks/badges, custom profile fields beyond core ones, and any extension data (portals, gallery, etc.).
Why is Sidekiq backed up after a Discourse import?
Each imported post triggers post-processing: Markdown rebake, link onebox, image downsize, search-index update. A 500k-post forum queues several hundred thousand jobs. Watch with Sidekiq::Stats.new.enqueued in Rails console; bump UNICORN_SIDEKIQS=4 in containers/app.yml to drain faster. Don't open the site to users until the queue is empty.
Can I migrate vBulletin to Discourse the same way?
Yes — same directory, different script. vbulletin.rb for v3/v4, vbulletin5.rb for v5. Identical pattern: container DB access, attachment dir mounted, env/YAML for credentials. The known wart is custom BBCode tags (vBulletin admins can register arbitrary ones) — those survive as plain text and need a discourse remap --regex pass after import.
How long does a phpBB → Discourse migration take?
Roughly 1-3 hours of script runtime per 100k posts on modest hardware, but Sidekiq drain afterward typically takes 2-5x longer than the import itself. Plan a 24-48 hour window for a 1M-post forum from import start to fully-baked, search-ready forum. Stage on a copy first so production downtime is minimized.
Do I need to upgrade phpBB before importing?
If you're on phpBB 2.x, yes — the importer only supports 3.x. Upgrade 2.x → 3.0 → 3.3 using phpBB's UMIL, dump the upgraded MySQL, point the Discourse importer at it. From phpBB 3.0/3.1/3.2/3.3 you can import directly; 3.3 is the smoothest.

Related Discourse Guides

Sources