├── .github
└── workflows
│ ├── dockerhub-push.yml
│ └── dockerhub-readme.yml
├── Dockerfile
├── README.md
├── entryPoint.sh
├── healthCheck.sh
├── sshd_config
├── sync-dnsmasq.sh
└── sync-pihole.sh
/.github/workflows/dockerhub-push.yml:
--------------------------------------------------------------------------------
1 | name: Push to DockerHub
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | paths-ignore: [ README.md ]
7 | pull_request:
8 | branches: [ master ]
9 | paths-ignore: [ README.md ]
10 |
11 | workflow_dispatch:
12 |
13 | # https://github.com/docker/build-push-action/blob/master/docs/advanced/multi-platform.md
14 | jobs:
15 | build-push:
16 | runs-on: ubuntu-latest
17 | steps:
18 | -
19 | name: Checkout
20 | uses: actions/checkout@v2
21 | -
22 | name: Set up QEMU
23 | uses: docker/setup-qemu-action@v1
24 | -
25 | name: Set up Docker Buildx
26 | uses: docker/setup-buildx-action@v1
27 | -
28 | name: Login to DockerHub
29 | uses: docker/login-action@v1
30 | with:
31 | username: ${{ secrets.DOCKERHUB_USERNAME }}
32 | password: ${{ secrets.DOCKERHUB_PASSWORD }}
33 | -
34 | name: Build and push
35 | uses: docker/build-push-action@v2
36 | with:
37 | context: .
38 | platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7
39 | push: true
40 | tags: shirom/pihole-sync:latest
41 |
--------------------------------------------------------------------------------
/.github/workflows/dockerhub-readme.yml:
--------------------------------------------------------------------------------
1 | name: Update Docker Hub Readme
2 | on:
3 | push:
4 | branches:
5 | - master
6 | paths:
7 | - README.md
8 | - .github/workflows/dockerhub-readme.yml
9 |
10 | workflow_dispatch:
11 | jobs:
12 | dockerHubDescription:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v2
16 |
17 | - name: Docker Hub Description
18 | uses: peter-evans/dockerhub-description@v2
19 | with:
20 | username: ${{ secrets.DOCKERHUB_USERNAME }}
21 | password: ${{ secrets.DOCKERHUB_PASSWORD }}
22 | repository: shirom/pihole-sync
23 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:3
2 |
3 | RUN apk -U update
4 | RUN apk add --no-cache dumb-init openssh-client openssh-server rsync inotify-tools bind-tools bash
5 |
6 | ADD sync-dnsmasq.sh /sync-dnsmasq.sh
7 | ADD sync-pihole.sh /sync-pihole.sh
8 | ADD entryPoint.sh /entryPoint.sh
9 | ADD sshd_config /
10 | ADD healthCheck.sh /healthCheck.sh
11 |
12 | ENTRYPOINT ["dumb-init", "/entryPoint.sh"]
13 |
14 | HEALTHCHECK --interval=30s --timeout=10s --retries=3 CMD /healthCheck.sh
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://hub.docker.com/repository/docker/shirom/pihole-sync)
2 |
3 | # docker-pihole-sync
4 | A Docker Container To Sync Two Piholes.
5 |
6 | ## Introduction
7 | A Pihole runs your entire network. If it goes down, your whole network goes down. If you have a family at home, they're going to be pretty annoyed that the wifi goes out everytime you want to do some maintainence. The only solution to this problem is to have a redundant pihole on your network, but you don't want to change your settings in two different places.
8 |
9 | This repo allows you to synchronize between two piholes where one is the master and one is the slave. I'll be adding support for more piholes in future. Just update one pihole and the rest automatically update. It supports the `/etc/pihole/` and `/etc/dnsmasq.d/` directories, excluding some directories which should be client-independent.
10 |
11 | It is based on Alpine 3.12 and utilizes `dumb-init`, `openssh`, `rsync`, `inotify-tools`, and `bash` for an image size of about 28 MB.
12 |
13 | Because Pi-Hole Docker utilizes a UID/GID of 0:0 and 999:999, this presents a unique problem for sending files over SSH, as the only user who can receive the files and maintain the proper UID/GID flags is root. However, having a docker container SSH in to the root user of another host is undesirable for a number of security related reasons.
14 |
15 | By utilizing a `sender` container node on one Pi and a `receiver` container node on the other Pi, we're able to solve the issue of securely opening a root user to SSH, by having the `sender` container node SSH into the `receiver` container node, rather than the host. If the container were to be infiltrated, the infiltrater would have access to root only in the receiver container, and its mounted volumes.
16 |
17 | **NOTE**: The sending and recieving container are only necessary for solving permissions issues without giving root access to the recieving container. If you have no problem giving root access to the recieving end (at the cost of security), or your recieving Pihole is not running in Docker, you don't need to use the recieving container.
18 |
19 | ## Why Docker PiHole Sync
20 |
21 | There are other options out there such as [pihole-cloudsync](https://github.com/stevejenkins/pihole-cloudsync) and [pihole-sync](https://github.com/simonwhitaker/pihole-sync), but this repo offers 4 unique features:
22 |
23 | ### 1. Docker Support
24 | If you have a project based on docker, it doesn't make sense to have a single sync script running outside of docker. Your whole project should be started with docker-compose up and ended on docker-compose down.
25 | ### 2. Continuous Synchronization
26 | The code will monitor the selected the folder for changes and immediately update the other Pihole. Great for updating the whitelist and seeing the website work immediately.
27 | ### 3. All Settings Are Transferred
28 | Not only are your lists transferred, but all your other settings are transferred as well including your password, upstream DNS settings, etc.
29 | ### 4. Keeps Your Github Clean
30 | Unlike [pihole-cloudsync](https://github.com/stevejenkins/pihole-cloudsync), we don't require a repository to sync to. This means that your Piholes don't have to connect to the internet, and you don't have a large number of commits going into a dummy repository. This is especially nice if you show private contributions on your profile and don't want a huge number of changes being published to your Github profile
31 |
32 | **NOTE**: The 'sender' Pihole must be able to SSH into the 'receiver' Pihole. If that's a restriction (maybe your Piholes are behind different VPNs), use [pihole-cloudsync](https://github.com/stevejenkins/pihole-cloudsync).
33 |
34 | ## Setup
35 | ### docker-compose.yml
36 |
37 | This is the `docker-compose.yml` for the sender/master Pi-Hole:
38 |
39 | ```yaml
40 | pihole:
41 | image: pihole/pihole:latest
42 | volumes:
43 | - /mnt/ext/pihole/etc-pihole:/etc/pihole
44 | - /mnt/ext/pihole/etc-dnsmasq.d:/etc/dnsmasq.d
45 | rest of pihole config...
46 |
47 | pihole-sync-sender:
48 | image: shirom/pihole-sync:latest
49 | container_name: pihole-sync-sender
50 | volumes:
51 | - /mnt/ext/piholesync/root:/root
52 | - /mnt/ext/pihole/etc-pihole:/mnt/etc-pihole:ro
53 | - /mnt/ext/pihole/etc-dnsmasq.d:/mnt/etc-dnsmasq.d:ro
54 | environment:
55 | - "NODE=sender"
56 | - "REM_HOST=(IP address of remote Pi)"
57 | - "REM_SSH_PORT=22222"
58 | ```
59 |
60 | This is the `docker-compose.yml` for the receiver/secondary Pi-Hole:
61 |
62 | ```yaml
63 | pihole:
64 | image: pihole/pihole:latest
65 | volumes:
66 | - /mnt/ext/pihole/etc-pihole:/etc/pihole
67 | - /mnt/ext/pihole/etc-dnsmasq.d:/etc/dnsmasq.d
68 | rest of pihole config...
69 |
70 | pihole-sync-receiver:
71 | image: shirom/pihole-sync:latest
72 | container_name: pihole-sync-receiver
73 | volumes:
74 | - /mnt/ext/piholesync/root:/root
75 | - /mnt/ext/piholesync/etc-ssh:/etc/ssh
76 | - /mnt/ext/pihole/etc-pihole:/mnt/etc-pihole
77 | - /mnt/ext/pihole/etc-dnsmasq.d:/mnt/etc-dnsmasq.d
78 | environment:
79 | - "NODE=receiver"
80 | ports:
81 | - 22222:22
82 | ```
83 |
84 | ### Volumes
85 | Volume | Function
86 | --- | --------
87 | `/mnt/ext/piholesync/root` | This is the directory in which the SSH key file and the known hosts file will be stored, so it needs to be persistent.
**Required on both nodes.**
88 | `/mnt/ext/piholesync/etc-ssh` | This is the directory in which the SSH server key files and the SSH daemon config will be stored, so it needs to be persistent. Can be a volume rather than a bind path, if you prefer.
**Required on the `sender` node only.**
89 | `/mnt/ext/pihole/etc-pihole` | This is the `/etc/pihole/` directory the Pi-Hole container writes to on the host filesystem. It is monitored and sychronized with the remote client directory. It should be set to the same as the /etc/pihole/ in the Pihole Docker container. See the compose file for details.
**Required on both nodes. Can be mounted read-only on the `sender` node.**
90 | `/mnt/ext/pihole/etc-dnsmasq.d` | This is the `/etc/dnsmasq.d/` directory the Pi-Hole container writes to on the host filesystem. It is monitored and sychronized with the remote client directory. It should be set to the same as the /etc/dnsmasq.d/ in the Pihole Docker container. See the compose file for details.
**Required on both nodes. Can be mounted read-only on the `sender` node.**
91 |
92 | ### Environment Variables
93 | Variable | Function
94 | --- | --------
95 | `NODE` | This is where you should define if the container is the `sender` or the `receiver`.
**Required on both nodes.**
96 | `REM_HOST` | This is the IP address (or FQDN/Hostname) of the remote Pi that we're syncting to.
**Required on the `sender` node only.**
97 | `REM_SSH_PORT` | This is the non-standard SSH port that should be exposed on the container. Default of 22222 is probably fine. However, if you change this on the `sender` node, be sure to change the exposed port forward on the `receiver` node.
**Required on the `sender` node only.**
98 |
99 | ### Ports
100 | Port | Function
101 | --- | --------
102 | `22222` | This is the port you want to expose for rsync/ssh. Your host is likely using 22 for SSH already, so it should be a non-standard port. The default of 22222 is probably fine. However, if you change this on the `receiver` node, be sure to change the `REM_SSH_PORT` on the `sender` node.
**Required on the `receiver` node only.**
103 |
104 | ## Support Information
105 | - Shell access while the container is running: `docker exec -it pihole-sync /bin/bash`
106 | - Logs: `docker logs pihole-sync`
107 | - Note the SSH-key instructions:
108 |
109 | On the receiver node, create an authorized_keys file in the config directory
110 | For example, if your 'config' volume mount on the receiver is:
111 | /docker/config/piholesync/root:/root"
112 |
113 | Then you would create a file at:
114 | /docker/config/piholesync/root/.ssh/authorized_keys"
115 |
116 | Copy/paste the contents between the ##### markers into that authorized_keys file:"
117 |
118 | ####### COPY BELOW THIS LINE, BUT NOT THIS LINE ########"
119 | /root/.ssh/id_ed25519.pub"
120 | ####### COPY ABOVE THIS LINE, BUT NOT THIS LINE ########"
121 |
122 | Once done, re-start the containers.
123 |
124 |
125 | ## Building Locally
126 | If you want to make local modifications to this image for development purposes or just to customize the logic:
127 | ```
128 | docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7 --tag shirom/pihole-sync --output type=image,push=false .
129 | ```
130 | For multi-arch builds, make sure you have [QEMU](https://medium.com/@artur.klauser/building-multi-architecture-docker-images-with-buildx-27d80f7e2408) installed
131 |
--------------------------------------------------------------------------------
/entryPoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | fail="1"
4 | if [[ -z "${NODE,,}" ]]; then
5 | fail="0"
6 | elif [[ "${NODE,,}" == "sender" ]]; then
7 | fail="0"
8 | elif [[ "${NODE,,}" == "receiver" ]]; then
9 | fail="0"
10 | elif ! grep -q "/mnt/etc-pihole" /etc/mtab; then
11 | echo "Please define a /mnt/etc-pihole config path."
12 | echo "This should be a physical path, such as:"
13 | echo "-v \"~/etc-pihole:/mnt/etc-pihole\""
14 | fail="1"
15 | elif ! grep -q "/mnt/etc-dnsmasq.d" /etc/mtab; then
16 | echo "Please define a /mnt/etc-dnsmasq.d config path."
17 | echo "This should be a physical path, such as:"
18 | echo "-v \"~/etc-dnsmasq.d:/mnt/etc-dnsmasq.d\""
19 | fail="1"
20 | fi
21 | if [[ "${fail}" -eq "1" ]]; then
22 | echo "Please set environmental flag 'NODE=[sender|receiver]'"
23 | exit 1
24 | fi
25 |
26 | if [[ "${NODE,,}" == "sender" ]]; then
27 | if ! grep -q "/root" /etc/mtab; then
28 | echo "Please define a root config path."
29 | echo "This should be a physical path, such as:"
30 | echo "-v \"~/piholesync/root:/root\""
31 | exit 2
32 | fi
33 | if ! [[ -e "/root/.ssh/id_ed25519" ]]; then
34 | ssh-keygen -b 2048 -t ed25519 -f /root/.ssh/id_ed25519 -q -N ""
35 | chmod 400 "/root/.ssh/id_ed25519"
36 | echo ""
37 | echo "On the receiver node, create an authorized_keys file in the config directory"
38 | echo ""
39 | echo "For example, if your 'config' volume mount on the receiver is:"
40 | echo "-v /docker/config/piholesync/root:/root"
41 | echo ""
42 | echo "Then you would create a file at:"
43 | echo "/docker/config/piholesync/root/.ssh/authorized_keys"
44 | echo ""
45 | echo "Copy/paste the contents between the ##### markers into that authorized_keys file:"
46 | echo ""
47 | echo "####### COPY BELOW THIS LINE, BUT NOT THIS LINE ########"
48 | cat "/root/.ssh/id_ed25519.pub"
49 | echo "####### COPY ABOVE THIS LINE, BUT NOT THIS LINE ########"
50 | echo ""
51 | echo "Once done, re-start this conatiner."
52 | exit 0
53 | fi
54 | if ! [[ -e "/root/.ssh/known_hosts" ]]; then
55 | ssh-keyscan -p ${REM_SSH_PORT} ${REM_HOST} > /root/.ssh/known_hosts 2>/dev/null
56 | if [[ "${?}" -ne "0" || "$(wc -l "/root/.ssh/known_hosts" | awk '{print $1}')" -eq "0" ]]; then
57 | echo "Unable to initiate keyscan. Is the receiver online?"
58 | rm "/root/.ssh/known_hosts"
59 | exit 3
60 | fi
61 | fi
62 | rsync -a -P --exclude '01-pihole.conf' /mnt/etc-dnsmasq.d/ -e "ssh -p ${REM_SSH_PORT}" root@${REM_HOST}:/mnt/etc-dnsmasq.d/ --delete
63 | if [[ "${?}" -ne "0" ]]; then
64 | echo "Unable to initiate dnsmasq.d rsync. Is the receiver online?"
65 | exit 4
66 | fi
67 | rsync -a -P --exclude 'install.log' --exclude 'pihole-FTL.conf' --exclude 'versions' --exclude 'dhcp.leases' --exclude 'setupVars.conf' --exclude 'setupVars.conf.update.bak' --exclude 'pihole-FTL.db' -e "ssh -p ${REM_SSH_PORT}" /mnt/etc-pihole/ root@${REM_HOST}:/mnt/etc-pihole/ --delete
68 | if [[ "${?}" -ne "0" ]]; then
69 | echo "Unable to initiate dnsmasq.d rsync. Is the receiver online?"
70 | exit 5
71 | fi
72 | chmod +x /sync-dnsmasq.sh /sync-pihole.sh
73 | ( /sync-dnsmasq.sh ) &
74 | /sync-pihole.sh
75 | fi
76 |
77 | if [[ "${NODE,,}" == "receiver" ]]; then
78 | if ! grep -q "/etc/ssh" /etc/mtab; then
79 | echo "Please define an /etc/ssh config path."
80 | echo "This should be a physical path, such as:"
81 | echo "-v \"~/piholesync/etc-ssh:/etc/ssh\""
82 | exit 6
83 | fi
84 | if ! grep -q "/root" /etc/mtab; then
85 | echo "Please define a root config path."
86 | echo "This should be a physical path, such as:"
87 | echo "-v \"~/piholesync/root:/root\""
88 | exit 7
89 | fi
90 | if ! [[ -e "/root/.ssh/authorized_keys" ]]; then
91 | echo "Please obtain the 'authorized_keys' file from the sender,"
92 | echo "and add it at your root/.ssh/authorized_keys path"
93 | exit 8
94 | fi
95 | sshKeyArr=("ssh_host_dsa_key" "ssh_host_dsa_key.pub" "ssh_host_ecdsa_key" "ssh_host_ecdsa_key.pub" "ssh_host_ed25519_key" "ssh_host_ed25519_key.pub" "ssh_host_rsa_key" "ssh_host_rsa_key.pub")
96 | for i in "${sshKeyArr[@]}"; do
97 | if ! [[ -e "/etc/ssh/${i}" ]]; then
98 | ssh-keygen -A
99 | fi
100 | done
101 | mv /sshd_config /etc/ssh/sshd_config
102 | # We can't SSH into the root user if it doesn't have a password set
103 | # Set a random 36 character string as the password
104 | rootPass="$(date +%s | sha256sum | base64 | head -c 36)"
105 | echo "root:${rootPass}" | chpasswd
106 | # Ensure permissions are correct on the root directory, or it won't let
107 | # us rsync/ssh in as the root user
108 | chmod 700 /root
109 | chmod 700 /root/.ssh
110 | chmod 600 /root/.ssh/authorized_keys
111 | /usr/sbin/sshd -D -e
112 | fi
113 |
--------------------------------------------------------------------------------
/healthCheck.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if [[ "${NODE,,}" == "sender" ]]; then
4 | if [[ -e "/fail" ]]; then
5 | exit 1
6 | fi
7 | elif [[ "${NODE,,}" == "receiver" ]]; then
8 | netstat -plant | grep -q :22
9 | if [[ "${?}" -ne "0" ]]; then
10 | exit 2
11 | fi
12 | fi
13 |
--------------------------------------------------------------------------------
/sshd_config:
--------------------------------------------------------------------------------
1 | Port 22
2 | AddressFamily any
3 | ListenAddress 0.0.0.0
4 | ListenAddress ::
5 | SyslogFacility AUTH
6 | LogLevel INFO
7 | LoginGraceTime 30s
8 | PermitRootLogin yes
9 | StrictModes yes
10 | MaxAuthTries 2
11 | MaxSessions 4
12 | PubkeyAuthentication yes
13 | AuthorizedKeysFile .ssh/authorized_keys
14 | PasswordAuthentication no
15 | PermitEmptyPasswords no
16 | ChallengeResponseAuthentication no
17 | AllowAgentForwarding yes
18 | AllowTcpForwarding yes
19 | GatewayPorts clientspecified
20 | X11Forwarding no
21 | PermitTTY yes
22 | PrintMotd no
23 | TCPKeepAlive yes
24 | PermitUserEnvironment no
25 | Compression delayed
26 | MaxStartups 10:20:30
27 | Subsystem sftp /usr/lib/openssh/sftp-server
28 |
--------------------------------------------------------------------------------
/sync-dnsmasq.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | inotifywait -r -m -e close_write --exclude '01-pihole\.conf' --format '%w%f' /mnt/etc-dnsmasq.d/ | while read MODFILE
3 | do
4 | rsync -a -P --exclude '01-pihole.conf' /mnt/etc-dnsmasq.d/ -e "ssh -p ${REM_SSH_PORT}" root@${REM_HOST}:/mnt/etc-dnsmasq.d/ --delete
5 | if [[ "${?}" -ne "0" ]]; then
6 | touch /fail
7 | elif [[ -f "/fail" ]]; then
8 | rm -f /fail
9 | fi
10 | done
11 |
--------------------------------------------------------------------------------
/sync-pihole.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | inotifywait -r -m -e close_write --exclude '((setupVars|setupVars|pihole-FTL)\.(conf|conf\.update\.bak|db)|local(branche|version)s|dhcp\.leases)' --format '%w%f' /mnt/etc-pihole/ | while read MODFILE
3 | do
4 | rsync -a -P --exclude 'install.log' --exclude 'pihole-FTL.conf' --exclude 'versions' --exclude 'dhcp.leases' --exclude 'setupVars.conf' --exclude 'setupVars.conf.update.bak' --exclude 'pihole-FTL.db' -e "ssh -p ${REM_SSH_PORT}" /mnt/etc-pihole/ root@${REM_HOST}:/mnt/etc-pihole/ --delete
5 | if [[ "${?}" -ne "0" ]]; then
6 | touch /fail
7 | elif [[ -f "/fail" ]]; then
8 | rm -f /fail
9 | fi
10 | done
11 |
--------------------------------------------------------------------------------