# Merge two Discourse sites into one

**URL:** https://meta.discourse.org/t/merge-two-discourse-sites-into-one/95435
**Category:** Migrating to Discourse
**Tags:** how-to
**Created:** [August 22, 2018, 9:11pm UTC](https://meta.discourse.org/t/merge-two-discourse-sites-into-one/95435 "2018-08-22T21:11:43Z")
**Posts on this page:** 6
**Page:** 1

<div class="post-metadata">

### Author: ![Discourse](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/discourse/32/148734_2.png) [@Discourse](https://meta.discourse.org/u/Discourse)
#### Post date: [August 22, 2018, 9:11pm UTC](https://meta.discourse.org/t/merge-two-discourse-sites-into-one/95435/1 "2018-08-22T21:11:43Z")

</div>

If you have two Discourse sites that you wish were one, this guide is for you.

There’s a tool called [discourse\_merger](https://github.com/discourse/discourse/blob/main/script/bulk_import/discourse_merger.rb) that can take one Discourse site and merge it into another.

# Prereqs

This is not an easy task, and should be treated like any other migration to Discourse. You will not be running discourse\_merger on a live production site. You will perform the merge in another environment where you can review the output before moving the result to production.

### Copy vs Merge

Almost everything will be **copied** from one site to the other, but Categories and Users can be **merged** , which will avoid duplication.

- Users will be merged if a user on both sites has the same email address.
- Categories will be merged if they have the same name.

If you want to do any reorganization of your data, do it before merging.

### Choose the destination site

Choose which site will be the destination for the data. This is the one that will retain all its styling and settings. The other site will have its users, categories, topics, posts, uploads, etc. copied/merged into the destination site.

# How to do it

Take backups of both sites _including files_ and copy them to the environment where you’ll perform the merge. It’s possible that they’re from different versions of Discourse, so we need them to be at the same version. I would choose to use the most recent version of Discourse while performing the merge.

Restore the destination site to the merger environment. If doing this from the command line:

```plaintext
bundle exec ruby script/discourse restore destination-2018-08-02-134227-v2018xxx.tar.gz

```

Next we’ll extract the other site.

```plaintext
cd /path/to/data
tar xvzf other-2018-08-02-134227-v2018xxx.tar.gz

```

The output will include the database dump and the upload files.

Create a database with the data:

```plaintext
psql
CREATE DATABASE "copyme" ENCODING = 'utf8';
\q
gunzip < /path/to/data/other-2018-08-02-134227-v2018xxx.tar.gz | psql -d copyme

```

If you’re running the import in an official Docker container (recommended), you will need to reset the postgres password to provide it to the script, otherwise you may encounter an error that the `postgres` user cannot access the database.

To change the password:

```plaintext
sudo -u postgres psql
\password postgres
(enter the new password)
\q

```

Now it’s time to run the script. Some env variables you’ll set:

**DB\_NAME** : name of database being merged into the destination site.  
**DB\_HOST** : _(optional)_ hostname of database being merged. leave blank if it’s local.  
**DB\_PASS** : password for the `postgres` user to access the database  
**UPLOADS\_PATH** : absolute path (site being merged) of the directory containing “original” and “optimized” dirs. e.g. /path/to/data/uploads/default  
**SOURCE\_BASE\_URL** : base url of the site being merged. e.g. [https://meta.discourse.org](https://meta.discourse.org)  
**SOURCE\_CDN** : _(optional)_ base url of the CDN of the site being merged.

You may need to run a bundle install prior to running the import script to avoid errors. To do so:

```plaintext
su discourse -c 'bundle config set --local with generic_import && bundle install'

```

On the first run, you might need to install some extra dependencies for the gems required in import.

Once the bundle is complete, run the import.

```plaintext
su discourse -c 'DB_NAME=copyme DB_PASS=password SOURCE_BASE_URL=http://copy.othersite.com UPLOADS_PATH=/shared/import/data/uploads/default bundle exec ruby script/bulk_import/discourse_merger.rb'

```

When it’s done, review the output in a web browser.

You can use the `remap` tool to update links from the old forum.

```plaintext
bundle exec ruby script/discourse remap 'copy.othersite.com' 'hot.newsite.com'

```

Also rebake all posts with uploads:

```plaintext
rake posts:rebake_match["upload:"]

```

If everything looks good, take a backup of the result and restore it to your production server.

```plaintext
bundle exec ruby script/discourse backup

```

> Last edited by @italo 2024-11-27T00:52:10Z
> 
> Last checked by @JammyDodger 2024-05-26T21:20:00Z
> 
> > **Check document**
> >
> > Perform check on document:

---

<div class="post-metadata">

### Author: ![pfaffman](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/pfaffman/32/120154_2.png) [@pfaffman](https://meta.discourse.org/u/pfaffman)
#### Post date: [June 20, 2024, 5:16pm UTC](https://meta.discourse.org/t/merge-two-discourse-sites-into-one/95435/52 "2024-06-20T17:16:19Z")

</div>

It seems to work, but when I run a backup I get

```plaintext
pg_dump: error: query failed: ERROR: permission denied for table migration_mappings                                                                                   

```

That’s very strange.

EDIT: resolved with

```plaintext
ALTER USER discourse WITH SUPERUSER;

```

---

<div class="post-metadata">

### Author: ![AstonJ](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/astonj/32/215041_2.png) [@AstonJ](https://meta.discourse.org/u/AstonJ)
#### Post date: [August 18, 2024, 1:57am UTC](https://meta.discourse.org/t/merge-two-discourse-sites-into-one/95435/53 "2024-08-18T01:57:31Z")

</div>

Has anyone used this recently? How did it go?

Also does anyone know if it’s possible to automatically put users into a group for each originating forum too? (To make it easier to give them permissions to view the the topics from the forum/s they came from.)

---

<div class="post-metadata">

### Author: ![pfaffman](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/pfaffman/32/120154_2.png) [@pfaffman](https://meta.discourse.org/u/pfaffman)
#### Post date: [August 18, 2024, 8:42am UTC](https://meta.discourse.org/t/merge-two-discourse-sites-into-one/95435/54 "2024-08-18T08:42:50Z")

</div>

It was a little rough. I think I had to comment out some stuff. There was also an issue with the merger images.

I think I’d add all the users of the new site to some group so that they’d be in that group when you merged them. This would be easier than doing it after or as part of the merge

---

<div class="post-metadata">

### Author: ![pfaffman](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/pfaffman/32/120154_2.png) [@pfaffman](https://meta.discourse.org/u/pfaffman)
#### Post date: [January 11, 2025, 7:57pm UTC](https://meta.discourse.org/t/merge-two-discourse-sites-into-one/95435/55 "2025-01-11T19:57:44Z")

</div>

Last time I did this the uploads from the merged site were all missing. I got a list of the uploads from `tar tf backupfile.tar.gz` and put them in `allfiles.txtx` and copied it to the uploads directory. This script (which will likely not work for you without modification) created an upload for each of those files, so then rebaking the posts fixed all (or most?) of the missing images.

```ruby
def process_uploads
  begin
    # Read the list of filenames
    filenames = File.readlines('/shared/uploads/allfiles.txt').map(&:strip)
    count = 0

    filenames.each do |filename|
      # Prepend /shared to the filename
      filename.gsub!(/\.\//,"")
      full_path = File.join('/shared/uploads/default/original/', filename)

      begin
        # Check if path exists and is a regular file (not a directory)
        count += 1
        
        if File.exist?(full_path) && File.file?(full_path)
          # Open the file
          File.open(full_path, 'r') do |tempfile|
            # Create upload using the specified parameters
            u = UploadCreator.new(tempfile, 'imported', {}).create_for(-1)
            puts "#{count} -- #{u.id}: #{u.url}"
          end
        else
          puts "Warning: Path not found or is not a regular file: #{full_path}"
        end
      rescue => e
        puts "Error processing file #{full_path}: #{e.message}"
        # Continue with next file even if current one fails
        next
      end
    end
  rescue Errno::ENOENT
    puts "Error: Could not find files.txt"
  rescue => e
    puts "Error reading files.txt: #{e.message}"
  end
end

# Execute the processing
process_uploads;

```

I got the bad posts like this:

```plaintext
 bad=Post.where("cooked like '%/images/transparent.png%'")

```

and then this to mark them as needing a rebake:

```plaintext
bad.update_all(baked_version: nil)

```

I was impatient, so I used

```plaintext
rake posts:rebake_uncooked_posts

```

to rebake them.

---

<div class="post-metadata">

### Author: ![AstonJ](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/astonj/32/215041_2.png) [@AstonJ](https://meta.discourse.org/u/AstonJ)
#### Post date: [January 25, 2025, 1:36am UTC](https://meta.discourse.org/t/merge-two-discourse-sites-into-one/95435/56 "2025-01-25T01:36:40Z")

</div>

I’m wondering whether it might just be easier to convert the discourse forum I want to merge to xenforo (their importers are usually excellent) then merge it with the other forums I want to be part of the merge (which themselves will be converted to xenforo from vbulletin) and then finally import the new merged xenforo forum into discourse..
