# Recipebook Backup

*2024-06-14*

> Saving those all-important food recipes


While I was busy studying for my [AWS certifications]({{< relref "getting-aws-certified" >}}), my other half decided to finally tackle a project they'd been meaning to do for a while: an application to store all the recipes they'd collected from various sources over the years.

They built the front-end with the [Tauri](https://tauri.app/) framework and, since the application would only ever be used at home, stored the data in a [MongoDB](https://www.mongodb.com/) database hosted on the Mac Mini running as [our home server]({{< relref "docker-at-home" >}}).

There was always the chance, however, that the server might fail – or that the data would need to be rolled back for some reason. Rather than lose a decade's worth of collected recipes, I decided to implement a backup strategy that would automatically push daily backups of the database up to S3.

As it happened, this dovetailed nicely with my certification studies. It's one thing to read about S3 lifecycle policies, quite another to actually configure one.

## Creating the Backup Script

As it turns out, MongoDB ships with a `mongodump` [command](https://www.mongodb.com/docs/database-tools/mongodump/) that can be executed against a running server to create a backup. All that was left was to wrap it in a script that executes the dump and uploads the archive to S3.

### recipebook-backup.sh

Since the script would eventually run inside a Docker container on the same Docker network as MongoDB, the `mongodump` command is executed against the `host.docker.internal` hostname on port 27017:

```shell
daystokeep=90
currentdate=$(date +"%Y%m%d%H%M")
filename=RecipeBook_$currentdate.gz

mongodump --host="host.docker.internal:27017" --db=RecipeBook --archive=/data/$filename --gzip

aws s3 sync /data/ s3://maceage-recipebook-backups --exclude "*" --include "*.gz"

find /data/ -type f -iname '*' -mtime +$daystokeep -exec rm -v "{}" +
```

The current date is used to generate the archive filename, which is then synced to S3. Finally, a separate command cleans up any local backups older than 90 days, so the container doesn't quietly fill the disk over time.

### crontab.txt

Running a cron job inside a Docker container turns out to be [relatively straightforward](https://stackoverflow.com/questions/37015624/how-to-run-a-cron-job-inside-a-docker-container) - a `crontab.txt` entry runs the script at 1am every day. I also wanted to capture any errors from the shell script, so the output is appended to a log file:

```text
0 1 * * * /recipebook-backup.sh >> /var/log/recipebook-backup.log
```

### entry.sh

The container also needs an entry point script to start the cron daemon in the foreground:

```shell
#!/bin/sh

# start cron
/usr/sbin/crond -f -l 8
```

### Dockerfile

Next, the Dockerfile for building the container uses the [ridiculously small](https://hub.docker.com/_/alpine) Alpine base image, adds the required files, and installs the necessary dependencies:

```text
FROM alpine
ADD crontab.txt /
ADD recipebook-backup.sh /
COPY entry.sh /
RUN apk add --no-cache mongodb-tools && \
    apk add --no-cache aws-cli && \
    chmod +x /recipebook-backup.sh && \
    chmod +x /entry.sh && \
    /usr/bin/crontab /crontab.txt

CMD ["/entry.sh"]
```

Building the container is then merely a case of running `docker build .` in the directory containing the Dockerfile.

### docker-compose.yml

With the image built, the final step is a `docker-compose.yml` that passes the AWS credentials in as environment variables:

```yaml
services:
  recipebook-backup:
    image: maceage/recipebook-backup
    build: .
    container_name: recipebook-backup
    hostname: recipebook-backup
    restart: unless-stopped
    environment:
      - AWS_ACCESS_KEY_ID
      - AWS_SECRET_ACCESS_KEY
      - AWS_DEFAULT_REGION
    volumes:
      - ${VOLUMES_DATA}:/data
    stdin_open: true
    tty: true
    labels:
      - "com.centurylinklabs.watchtower.enable=false"
```

The container is then started with `docker-compose up -d`, and from that point onwards it performs a nightly backup of the MongoDB database to S3 without any further intervention.

## S3 Lifecycle Policy

On the AWS side, the S3 bucket needs to be created and the credentials configured. These are then passed to the container via the `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_DEFAULT_REGION` environment variables.

Although the backup files themselves are tiny – a gzip-compressed dump of a recipe database doesn't exactly strain the network - I wanted to ensure only the most recent backups were kept. A lifecycle policy on the S3 bucket deletes backups older than 365 days, which gives a comfortable window: even if a problem went unnoticed for months, there would still be a healthy history of restores to choose from.

## Wrapping Up

Quite possibly the simplest Docker container I've ever built, and it's been running without a single issue. There's something satisfying about infrastructure you can safely forget about.

The only ongoing requirement is rotating the AWS keys from time to time – although I'm not sure who would be interested in our super-secret food recipes even if they did get hold of them :rofl:
