My take on syncing 2 piholes - pihole-gemini two-way Pi-hole lists sync

Pi-hole Gemini (Two-way Pi-Hole lists sync) Readme - 03-12-2019

Based on Dual pihole sync 2.0 script ( which was based on Sync two PiHoles bash script ( by

While I personally started with LandlordTiberius' script, I made changes for my personal setup, and thought that my version of the script could be useful to others who are running 2 Pi-holes that aren't using the DHCP and just want to keep their white lists, black lists, block lists and gravity synced between 2 Pi-holes that also didn't want the sync to be on a timer (cron) or using a file monitoring utility like inotify.

The pihole-gemini script can be found here:


  • 2 systems running as Pi-holes
  • SSH access enabled on both systems
  • A user on each system with sudo permission and ssh access (the username MUST be the same on both for this script)
  • rsync should be installed on both systems

The main purpose of this script is to keep the lists of 2 Pi-holes in sync. While there are other scripts out there that do a great job of keeping the black and white lists synchronized between two Pi-holes, I wasn't happy with how they were being triggered. Basically, I didn't want to wait for a cron to run to push an update, and frankly, didn't want cron jobs firing off when I felt that I didn't need them to, but I also didn't want to run a service like inotify (as lean as it is) to monitor the files for changes. After lots of digging around Pi-hole's files, I decided the best place to trigger my script from was the end of Pi-hole's script. This afforded me the maximum amount of integration with Pi-hole as the script is triggered whenever the script is run. The script is triggered whenever something is added or removed from a white or black list, when adding new block lists, or removing, enabling or disabling existing block lists. So pihole-gemini will run whenever gravity is updated, including when Pi-hole's gravity is updated from the command line using the "pihole -g" command.

With the number of changes and additional logic I've added to my version of the script, I guess this is really more of a 'fork' than an update. As such, I've decided to name it "Gemini" (the interstellar twins) since I couldn't find any references to interstellar clones, I figured interstellar twins was the closet celestial body to imply, well, two of something very similar.


  • A good amount of logging information has been added to be able to go back and look at recent jobs to make sure everything ran as expected.
  • The script is designed for you to set both ip addresses in one script and simply use that one script on both Pi-holes without having to make custom edits on each Pi-hole to define the 'other' Pi-hole's ip address.
  • Ability to define custom ports. The script was written to allow for the configuration of custom SSH ports. This allows you to use a custom port (instead of the default 22) for SSH connections. The port can be defined for each connection, so both Pi-holes could use different ports for SSH. This is in case a secondary Pi-hole is running in a VM using a different non-standard port than the primary Pi-hole.

Benefits of running sync from the script:

  • The script runs automatically when it needs to. It does not use cron or file monitoring to be triggered.
  • When using the Update Gravity page in the web interface, or when adding or removing block lists from the Settings page, the results of the sync job(s) are displayed along with the blocklist information.
  • When updating gravity, a "local" gravity update will also trigger a "remote" gravity update, making this a 2-way sync on gravity updates. This happens from both the web interface and from using the pihole -g command at the prompt.

Logging information:

  • The script creates a new log file every day, and every job for the day is appended to the file.
  • The default directory I'm using for logs is currently /tmp. You can change this in the script's user-defined variables section, and you *SHOULD* change it if you wish to preserve the log files. Log files in the /tmp directory are automatically deleted on system restarts. If you do change the log directory, be sure to set the LOGKEEPDAYS variable to ensure that old log files are cleaned up at the interval you desire.

NOT FOR DHCP Configurations:
The current version of the script does NOT sync DHCP files. If you are using Pi-hole for DHCP configurations, I would recommend finding another script that DOES use cron (for scheduled checks). This is because, for redundancy, a primary DHCP server should be tested at regular intervals by the backup server, so the backup can take over if the primary is down. I would also run something like inotify on the primary DHCP server to keep things like the lease files in sync. This puts DHCP redundancy well outside the scope of pihole-gemini, as the script is only designed for keeping white lists, black lists, block lists and gravity in sync. Because of the way this script is designed to be triggered and used, it is simply not capable of keeping DHCP stuff synchronized in any meaningful manner that would be usable for providing DHCP redundancy.

Other notes:

  • While the script does keep the lists synchronized on two Pi-holes, be aware that all other aspects of the Pi-Holes are completely independent, including the statistics displayed on the respective Pi-hole admin pages, and the disable functions. So if you need to disable blocking, you will need to have both Pi-hole admin pages open, and manually disable blocking on EACH ONE for the time period you want it disabled for. I am still trying to find where the disable functions from the web interface are located, and if there's a way to "piggyback" a script to the function in the way the pihole-gemini script piggybacks the script.
  • ALL gravity updates triggered by pihole-gemini are triggered using the --skip-download option to prevent the block lists from redownloading. The script should never have to trigger a full gravity update including down-loading the block lists, since gravity updates are what trigger the script.

This version of jvinch76's sync two piholes bash script (pihole-gemini) was written by

This script originally started life as

The modifications I've made were actually based on the updated version at

Setting up and configuring the pihole-gemini script. The script can be found at

These steps need to be taken on both piholes.

1.) Log in to Pi-hole as the user that will be used for running the sync process. Make sure this user has both ssh and sudo access to the local Pi-hole system.

2.) Change to the /usr/local/bin directory.
$ cd /usr/local/bin

3.) Create the script file and open it for editing.
$ sudo nano pihole-gemini

4.) Paste the script into the pihole-gemini script file.

5.) Change the values in the USER-DEFINED VARIABLES section to match your setup.

6.) Save the script (ctrl+o, then ) and exit the editor (ctrl+x).

7.) Make the script executable.
$ sudo chmod +x pihole-gemini

8.) Create an ssh key to allow remote connections without supplying a password for the user that you're using to sync files between Pi-holes (the connections will use the key generated here instead.)
$ ssh-keygen

Answer the prompts (leaving them blank will use the default values) to generate the ssh key.

If you get a permission denied error, you may need to manually create the .ssh folder in the home folder of the user that will be used to sync the files, then make sure the correct user owns the folder. The example below uses the 'pi' user. If you did not get a permission denied error, you can skip to step 9.
$ cd ~
$ sudo mkdir .ssh
$ sudo chown user:group .ssh

 So for the user pi, the command would be:

$ sudo chown pi:pi .ssh

Now you should retry the ssh-keygen command (start step 8 over.)

9.) Check that the ssh service is running.
$ eval ssh-agent

If you get a response like "Agent pid 1234", then the service is running. Note that the numbers 1234 are for demonstrative purposes, and the actual number displayed on your system will be different.

10.) Add the ssh key to the local Pi-hole. Once you are sure the ssh service is running, then add the key, being sure to use the filename you created when you ran ssh-keygen. If you left it blank, it will be the default filename (id_rsa), which is what I'm using in this example.
$ ssh-add id_rsa
If you set a passphrase during ssh-keygen, you will be prompted for the passphrase in order to add the key.

11.) Send the key to your 'other' pi-hole system. You should use the 'other' pi-hole's username @ the other pi-hole's ip address in the command
$ ssh-copy-id other-pi-username@other-pi-ip-address

12.) After configuring both pi-hole's ssh keys, test the ssh login from the command line.

If you are NOT using a custom port for ssh, use
$ ssh username@other-pihole-ip

If you ARE using a custom port # for ssh, substitute your port # for the ## in the example below.
$ ssh -p ## username@other-pihole-ip

On your first login, it may prompt you for the passphrase you set in ssh-keygen (if you set one.) Enter the passphrase, and immediately after logging in, issue the "exit" command to disconnect from the 'other' pihole, and try the ssh command again. You should not be prompted for the passphrase after entering it the first time.
Once you've confirmed the ability to log in without having to supply a password or passphrase (ensuring the ssh key is working), you can issue the "exit" command to close the ssh session and return to the local prompt, however, you could perform step 13 remotely to get the 'other' pi-hole integrated before closing the connection. If you wish to do this, perform step 13 before issuing the "exit" command, and then perform step 13 again (this time locally) to finish full pi-hole integration.

13.) Finally, we need to integrate the script into Pi-hole. We will do this by editing Pi-hole's script, but first, we'll back it up.
$ sudo cp /opt/pihole/ /opt/pihole/

Then we'll edit the file
$ sudo nano /opt/pihole/

Press the or key on your keyboard and hold it down until you get to the bottom of the file.

The very last line should read:
"${PIHOLE_COMMAND}" status

We will be adding a new command directly ABOVE that line, so that "${PIHOLE_COMMAND}" status remains the last line of the file. The line we need to add is:
su -c '/usr/local/bin/pihole-gemini' - pi

Note that the "pi" at the end of the line should be replaced with the username of your sync user account. Once we're done editing it, we can save (ctrl+o, then ) and exit the editor (ctrl+x).

Once you've finished step 13, you're done. You can invoke the script directly by calling pihole-gemini at the command line, and should do so, to manually test the script to ensure everything is working as expected. From now on, it will run automatically whenever you update gravity, add or remove items from the white or black list, or add or remove items from the block list (including enabling or disabling block lists).

Important note: When upgrading to a new version of Pi-hole, you may have to repeat step 13 if the file gets updated in order to re-enable the pihole-gemini sync.

Link to script:

(Edited to fix some formatting errors.)


Thank you for sharing your work!

I had to modify my Sudoers file to make it fully work without a hitch though, and may not be for everyone as it is a security risk.

Details: Primary Pihole is a Ubuntu VM, and the other a Pi3 running Dietpi.

This is exactly what I've been looking for. Thank you and great work.

I had to make one minor change as I run avahi on the same pi, I have two vlan interfaces running. The script pulls the last ip from the list, which in my case wasn't the one I wanted.

Changed line 91 from
LOCALINTERFACE=$(ip -4 route | awk '{print $3}' | tail -n 1)
LOCALINTERFACE=$(ip -4 route | awk '{print $3}' | head -2 | tail -n 1)

and its working a treat.

Again thank you.

1 Like


This is exactly what I need!
I have done all steps but when running the script (manually) I'm getting prompted for a password.

2019-08-08 13:01:58 - pihole-gemini v0.0.2.2a was successfully launched as user: pi
             is updating Pi-hole on
2019-08-08 13:01:58 - Testing SSH Connection to pi@ Please wait.
2019-08-08 13:02:00 - SSH Connection to pi@ was tested successfully.
2019-08-08 13:02:00 - Comparing local to remote black.list and updating if neccesary.
pi@'s password:

Both machines have the same username "pi".
I have imported the SSH key on both machines and test if I can login.
On both machines the user "pi" is in the sudoers file so I can do sudo without entering the password.

sudoers file:


Any one got a clue?

Can you elaborate?
I think I’m facing the same problem you had...

I am not sure what step you are stuck on, so I will give you an example for editing the Sudoers file:
sudo visudo

Change this line:

# Members of the admin group may gain root privileges
%admin  ALL=(ALL) ALL

to this line:

# Members of the admin group may gain root privileges

And move it under this line:

# Allow members of group sudo to execute any command
%sudo   ALL=(ALL:ALL) ALL

you should now have this:

# This file MUST be edited with the 'visudo' command as root.
# Please consider adding local content in /etc/sudoers.d/ instead of
# directly modifying this file.
# See the man page for details on how to write a sudoers file.

Defaults        env_reset
Defaults        mail_badpass
Defaults        secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

# Host alias specification

# User alias specification

# Cmnd alias specification

# User privilege specification
root    ALL=(ALL:ALL) ALL

# Allow members of group sudo to execute any command
%sudo   ALL=(ALL:ALL) ALL

# Members of the admin group may gain root privileges

# See sudoers(5) for more information on "#include" directives:

#includedir /etc/sudoers.d

then for every user that needs sudo access WITH a password:

sudo adduser <user> sudo

and for every user that needs sudo access WITH NO password:

sudo adduser <user> admin

and finally, run this:

sudo service sudo restart

And that's it!

Edit: You may have to add the admin group as I don't think it exists by default.

sudo groupadd admin


Thanks for your reply!
My sudoers file looks like this:

# This file MUST be edited with the 'visudo' command as root.
# Please consider adding local content in /etc/sudoers.d/ instead of
# directly modifying this file.
# See the man page for details on how to write a sudoers file.
Defaults        env_reset
Defaults        mail_badpass
Defaults        secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin"

# Host alias specification

# User alias specification

# Cmnd alias specification

# User privilege specification
root    ALL=(ALL:ALL) ALL

# Members of the admin group may gain root privileges
%admin ALL=(ALL) ALL

# Allow members of group sudo to execute any command
%sudo   ALL=(ALL:ALL) ALL

# See sudoers(5) for more information on "#include" directives:

#includedir /etc/sudoers.d

This should be enough right?
Giving the user "pi" sudo rights without password prompt.
Strange thing is I still get the password prompt when the first list (black.list) is being synced.
Running other commands with sudo I don't have the password prompt.

Just build a new VM with Debian (Buster/10) as OS.
Same problem...
Sycning from the Raspberry to the VM does not work and gives me the password problem.

Only thing different is that my Raspberry Pi is running on Raspbian Lite (Buster) instead of DietPi.

Got to the same password prompt, with Raspbian Lite (Buster). Please advise...

I need to pull up my file and compare. Sorry it has been a long
time since I set this up.

1 Like



1 Like


I've installed DietPi (Buster image) on my Rpi, just to test.
But same problem...

Do you run the latest Buster image of DietPi or still using the Stretch release?

Sorry I have been super busy with work, I will check my settings
later today and post.

No worries mate. Thanks in advance!

@GeorgeT @ckishappy @9v9

I can confirm there is a problem with Raspbian Buster (and this script).
I have switched to Raspbian Stretch (2019-04-08-raspbian-stretch-lite) on my Raspberry Pi and switched my Virtual Machine to Debian Stretch (9.9) and I can sync both ways now manually!

[update 1]
Manually running pihole-gemini works but it does not does not sync automatically...
Adding something on the whitelist or blacklist does not sync.

[update 2]
Got it working now automatically as well!

I have changed the line which is added to /opt/pihole/ a bit.

Instead of:
su -c ‘/usr/local/bin/pihole-gemini’ - pi

I now use:
su -c /usr/local/bin/pihole-gemini - pi

Same command, except I removed the single quotes.

1 Like

Sorry I did not get back to you in time. Glad you got it working, just note when an update to PiHole comes out you have to go into that and add the line again, so make sure you keep it somewhere safe.

I run Dietpi so I am not sure whats going on with Rasbian Buster. The more minimal the image the better IMHO.

If you have more questions like using them in a DNS 'Failover' let me know?

No worries! Got them going good now. :smiley:

Of course you may tell me more about using them in a failover. I’m really interested!

Depending on your Router and situation you can make sure that all your devices go through the PiHole(s), whilst everything else using a custom DNS is blocked.

It's more advanced and requires special attention to your particular setup. Your best bet is to look around and see what settings other people are using. I Use iptables to control this at the router level, and both DNS rules are in strict order: Pihole#1 is main DNS, if it fails it switches to PiHole#2. I don't use DHCP features of the PiHoles so this setup is easier to maintain.

Hey guys,

Love the work. I wanted to update everyone on this and maybe the creator as well. I was having the same problem as @Kroontje with respect to double IP's during compare stage. It has to do with something that was change in the Buster version of Raspbian. Either way, the script needs a slight modification to make it work.

If you are running Buster you need to change line 171 from this

RSYNC_COMMAND=$(rsync --rsync-path='/usr/bin/sudo /usr/bin/rsync' -aiu -e "ssh -l $HAUSER@$REMOTEPI -p$SSHPORT" $PIHOLEDIR/$FILE $HAUSER@$REMOTEPI:$PIHOLEDIR)

to this

RSYNC_COMMAND=$(rsync --rsync-path='/usr/bin/sudo /usr/bin/rsync' -aiu -e "ssh -l $HAUSER -p$SSHPORT" $PIHOLEDIR/$FILE $HAUSER@$REMOTEPI:$PIHOLEDIR)

This removes the first @$REMOTEPI in the line and allows the script to run without douple IP issues. Again only in the Buster variant. I am running full on a 3b+ syncing with a zero w on stretch. I do not know if it works with the lite version but give it a shot. It may be best for OP to make two scripts, one for Buster, and one for everything else. Hope this helps anyone who runs into this issue. Thanks again for putting in the leg work.