Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ build
/repositories
repo/
db_backups
/secrets/mongo-replica-keyfile
message.txt

# macOS
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ docker-compose up -d

**4. Open** <http://localhost:5000>. The port can be changed in `docker-compose.yml`; putting Anonymous GitHub behind nginx is recommended for HTTPS.

For an optional remote, hidden, delayed MongoDB replica and backup source, see
the [MongoDB replication guide](docs/mongodb-replication.md).

</details>

## Scope of anonymization
Expand Down
17 changes: 17 additions & 0 deletions docker-compose.replica-primary.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
services:
mongodb:
entrypoint:
- /bin/bash
- /opt/anonymous-github/mongodb-replica-entrypoint.sh
command:
- --quiet
- --replSet
- rs0
- --keyFile
- /tmp/mongo-replica-keyfile
- --bind_ip_all
environment:
MONGO_REPLICA_KEYFILE_PATH: /run/secrets/mongo-replica-keyfile
volumes:
- ${MONGO_REPLICA_KEYFILE:-./secrets/mongo-replica-keyfile}:/run/secrets/mongo-replica-keyfile:ro
- ./scripts/mongodb-replica-entrypoint.sh:/opt/anonymous-github/mongodb-replica-entrypoint.sh:ro
38 changes: 38 additions & 0 deletions docker-compose.replica-secondary.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: anonymous-github-mongo-secondary

services:
mongodb-secondary:
image: ${MONGO_IMAGE:-mongo:latest}
restart: unless-stopped
entrypoint:
- /bin/bash
- /opt/anonymous-github/mongodb-replica-entrypoint.sh
command:
- --quiet
- --replSet
- rs0
- --keyFile
- /tmp/mongo-replica-keyfile
- --bind_ip_all
environment:
MONGO_REPLICA_KEYFILE_PATH: /run/secrets/mongo-replica-keyfile
volumes:
- mongodb_secondary_data:/data/db
- ${MONGO_REPLICA_KEYFILE:-./secrets/mongo-replica-keyfile}:/run/secrets/mongo-replica-keyfile:ro
- ./scripts/mongodb-replica-entrypoint.sh:/opt/anonymous-github/mongodb-replica-entrypoint.sh:ro
ports:
- ${MONGO_BIND_ADDRESS:-127.0.0.1}:27017:27017
healthcheck:
test:
- CMD
- mongosh
- --quiet
- --eval
- "db.adminCommand('ping')"
interval: 10s
timeout: 10s
retries: 12
start_period: 10s

volumes:
mongodb_secondary_data:
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,15 @@ services:
retries: 5

mongodb:
image: mongo:latest
image: ${MONGO_IMAGE:-mongo:latest}
restart: on-failure
environment:
MONGO_INITDB_ROOT_USERNAME: $DB_USERNAME
MONGO_INITDB_ROOT_PASSWORD: $DB_PASSWORD
volumes:
- mongodb_data_container:/data/db
ports:
- 127.0.0.1:27017:27017
- ${MONGO_BIND_ADDRESS:-127.0.0.1}:27017:27017
command: --quiet
healthcheck:
test:
Expand Down
200 changes: 200 additions & 0 deletions docs/mongodb-replication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
# Optional remote MongoDB replica

This deployment option converts the existing standalone MongoDB container into
a replica set named `rs0` and adds a remote, hidden, non-voting secondary.
Normal single-node deployments continue to use `docker-compose.yml` alone.

The remote member is designed as a recovery and backup source:

- It cannot become primary.
- Its availability does not affect production majority writes.
- It is hidden from application traffic.
- It is delayed by one hour by default, providing a short recovery window for
accidental deletes or a bad migration.

Replication is not a backup. Destructive changes eventually reach every
member. Continue making encrypted, off-host `mongodump` archives from the
secondary.

## Requirements

1. Take and verify a `mongodump` backup before converting production.
2. Install this repository on both servers.
3. Connect the servers through a trusted private network such as WireGuard,
Tailscale, a private VLAN, or a cloud private network.
4. Create stable DNS names that resolve from both servers and from the
application container, for example:

- `mongo-primary.internal`
- `mongo-secondary.internal`

5. Permit TCP port `27017` only between the required private-network hosts.
Do not expose MongoDB to the public internet.
6. Run the exact same MongoDB version on both servers. Find the current
production version with:

```bash
docker compose exec mongodb mongod --version
```

Set the matching image in both servers' `.env` files:

```env
MONGO_IMAGE=mongo:<exact-version>
```

## 1. Generate the shared member key

On the primary server:

```bash
./scripts/mongodb-replica.sh generate-key
```

This creates `secrets/mongo-replica-keyfile` without overwriting an existing
key. Copy that exact file securely to the same repository-relative path on the
secondary server:

```bash
scp secrets/mongo-replica-keyfile \
secondary-server:/path/to/anonymous_github/secrets/mongo-replica-keyfile
```

Keep this key outside source control and backups that are accessible to
untrusted users. Every replica-set member must share the same key.

## 2. Configure production

Add these values to the primary server's `.env`:

```env
# Use the exact version already running in production.
MONGO_IMAGE=mongo:<exact-version>

# The primary server's private/VPN interface.
MONGO_BIND_ADDRESS=<primary-private-ip>
MONGO_REPLICA_KEYFILE=./secrets/mongo-replica-keyfile

# Make the replica overlay the default for future Compose commands.
COMPOSE_FILE=docker-compose.yml:docker-compose.replica-primary.yml

# URL-encode special characters in the username and password.
MONGODB_URI=mongodb://<user>:<password>@mongo-primary.internal:27017/production?authSource=admin&replicaSet=rs0&retryWrites=true&w=majority
```

Stop application writers, leaving MongoDB available:

```bash
docker compose stop anonymous_github streamer
```

Start MongoDB with replica-set support and initialize the existing database as
the primary:

```bash
./scripts/mongodb-replica.sh primary-up \
mongo-primary.internal:27017
```

`primary-up` is idempotent. If `rs0` is already initialized, it reports that
state instead of replacing the replica-set configuration.

## 3. Start the remote secondary

On the secondary server, create a minimal `.env`:

```env
MONGO_IMAGE=mongo:<same-exact-version-as-primary>
MONGO_BIND_ADDRESS=<secondary-private-ip>
MONGO_REPLICA_KEYFILE=./secrets/mongo-replica-keyfile
```

Start its empty MongoDB data volume:

```bash
./scripts/mongodb-replica.sh secondary-up
```

Do not copy the primary's live WiredTiger volume. MongoDB performs the initial
sync after the member is added.

## 4. Add and verify the secondary

Back on the primary server, add the remote member with the default one-hour
delay:

```bash
./scripts/mongodb-replica.sh add-secondary \
mongo-secondary.internal:27017
```

To keep the remote copy current instead, explicitly set a zero-second delay:

```bash
./scripts/mongodb-replica.sh add-secondary \
mongo-secondary.internal:27017 0
```

Check initial-sync and replication state:

```bash
./scripts/mongodb-replica.sh status
```

Wait until the remote member reports `SECONDARY`, then restart the application:

```bash
docker compose up -d anonymous_github streamer redis
```

## Operations

Use the primary overlay for every future operation on the production stack.
Keeping `COMPOSE_FILE` in `.env` makes ordinary commands such as
`docker compose up -d` and `docker compose logs mongodb` use it automatically.

Check replica status:

```bash
./scripts/mongodb-replica.sh status
```

If a destructive production operation occurs and the secondary is delayed,
stop the secondary container immediately before the delay window elapses:

```bash
docker compose -f docker-compose.replica-secondary.yml stop mongodb-secondary
```

Then take a copy or logical dump of the delayed data before attempting
recovery.

For automatic failover, use three voting data-bearing members instead of
turning this two-server recovery topology into a two-voter replica set. Two
voters require both servers to acknowledge a majority and can make production
unwritable during a network outage.

## Troubleshooting

- All members must use the same replica-set name (`rs0`), MongoDB version, and
shared keyfile.
- The hostnames stored in `rs.conf()` must resolve from every member.
- The application container must also resolve `mongo-primary.internal`. Use
private DNS or a Compose `extra_hosts` entry if host DNS is not propagated
into Docker.
- Check container logs with:

```bash
docker compose logs mongodb
docker compose -f docker-compose.replica-secondary.yml logs mongodb-secondary
```

- Re-run `primary-up`, `add-secondary`, or `status` safely; the management
operations do not replace existing replica-set members.

## MongoDB references

- [Convert a standalone server to a replica set](https://www.mongodb.com/docs/manual/tutorial/convert-standalone-to-replica-set/)
- [Deploy a replica set with member authentication](https://www.mongodb.com/docs/v8.0/tutorial/deploy-replica-set-with-keyfile-access-control/)
- [Configure a delayed member](https://www.mongodb.com/docs/manual/tutorial/configure-a-delayed-replica-set-member/)
- [Hidden replica-set members](https://www.mongodb.com/docs/manual/core/replica-set-hidden-member/)
17 changes: 17 additions & 0 deletions scripts/mongodb-replica-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -Eeuo pipefail

source_keyfile="${MONGO_REPLICA_KEYFILE_PATH:-/run/secrets/mongo-replica-keyfile}"
runtime_keyfile="/tmp/mongo-replica-keyfile"

if [[ ! -s "$source_keyfile" ]]; then
echo "MongoDB replica keyfile is missing or empty: $source_keyfile" >&2
exit 1
fi

# Bind-mounted secrets commonly have host ownership or permissive modes that
# mongod rejects. Copy to an ephemeral location with the required ownership
# and permissions before the official image entrypoint drops privileges.
install -m 400 -o mongodb -g mongodb "$source_keyfile" "$runtime_keyfile"

exec /usr/local/bin/docker-entrypoint.sh "$@"
Loading
Loading