Backup Grafana SQLite with Litestream using s6-overlay in a container app
Tags: sqlite, litestream, s6overlay, containers
Published: 2023-10-07 04:30:42

I run my own small stack to monitor a set of nodes and use grafana to visualise all logs, metrics and events and deliver any alerts. For my use case running with sqlite is sufficient without adding an external dependency of postgres or mysql DB on the limited hardware that it runs on.

SQLite has recently gained lot of traction with Litestream, LiteFS and Turso making it a choice to be used in production at a larger scale than just on the edge for a smaller workload.

As grafana being a crucial component in my o11y stack backing up of the DB was equally crucial. There are many options ranging from running cron jobs and making copies upon booting and restarting to capture the state. None really captures the continuous changing state and there is greater chance of data loss between backups. 

[Litestream](https://litestream.io) is a game changer allowing to backup all your changes to a an s3 compatible cloud storage at 1sec intervals using the WAL all done via the API and not interacting with the DB itself to avoid corruption or impacting performance.

But how to run Litestream within a container, Isn't container suppose to run 1 process as best practice ? Yes and no, I like the mantra [s6-overlay](https://github.com/just-containers/s6-overlay) gives of 1 process that can run multiple process that are supervised and linked based on dependency. A supervisor co-ordinates and makes sure that they all work in harmony.

Now on to how to use litestream with s6-overlay. [benbjohnson](https://github.com/benbjohnson) creator of litestream has a repo over how to use [litestream and s6-overlay with a go app container](https://github.com/benbjohnson/litestream-s6-example/tree/main) but the repo is publicly archived. Its probably so becuase its uses s6-0verlay version 2 while the newer version is 3 and its got a new design over how to organise your application and define the run scripts and dependencies. 

s6-overlay has great instructions over how to set it up. It uses execline but most of the things are doable and most likely you won't face any shortcomings. In case of any limitations you can link to bash scripts to execute custom scripts.

 The setup I have is where `grafana` and `litestream` are long running jobs that continue to run for the life of the container while `litesream-restore` is a `oneshot` job that only runs at the startup. with that sequence `litestream` depends on `grafana` to be running and `grafana` requires that `litestream-restore` executes successfully to restore any snapshots available before starting. The dependency graph would be as such

```
 litesream-restore --> grafana --> litestream
```

To setup jobs the files need to be setup as such for this sequence.

```

- /etc
  |- /s6-overlay
   |-/s6-rc.d
    |-/grafana
     |- type
     |- run
     |- finish
     |-/dependencies.d
     |- litesream-restore
    |-/litestream
     |- type
     |- run
     |-/dependencies.d
      |- grafana
    |-/litestream-restore
     |- type
     |- up
     |-/dependencies.d
      |- base
    |-/user
     |-/contents.d
      |- grafana
      |- litestream
      |- litestream-restore
   |-scripts
    |-/litestream-restore
     |- 00-litestream
  |-litestream.yml

```

`type` content will either be `longrun` or `oneshot`. We have 1 oneshot and 2 longrun jobs. There content would be as such

`/etc/s6-overlay/s6-rc.d/grafana/type`
```
longrun
```
`/etc/s6-overlay/s6-rc.d/litestream/type`
```
longrun
```
`/etc/s6-overlay/s6-rc.d/litestream-restore/type`
```
oneshot
```

Each application have their `dependencies.d` folder where an empty file is be defined with the name of its dependency application folder. In case of a application not having a dependency a file with a name `base` is to be defined.

The dependency graph 

```
 litesream-restore --> grafana --> litestream
```
would be defined as such
`/etc/s6-overlay/s6-rc.d/grafana/dependencies.d/base`
`/etc/s6-overlay/s6-rc.d/grafana/dependencies.d/litestream-restore`
`/etc/s6-overlay/s6-rc.d/litestream/dependencies.d/grafana`

For `longrun` jobs a `run` file is to be defined while for `oneshot` `up` file is used. As oneshot could require complex steps and might be limited to execline you can define a bash script to be executed in the onshot up file.

To run oneshot litestream up define the path of the script file

`/etc/s6-overlay/s6-rc.d/litestream-restore/up`
```bash
/etc/s6-overlay/scripts/litestream-restore/00-litestream
```

`/etc/s6-overlay/scripts/litestream-restore/00-litestream`
```bash
#!/command/with-contenv bash

: ${MINIO_LITESTREAM_ENDPOINT?"Need to set MINIO_LITESTREAM_ENDPOINT environment variable"}
: ${MINIO_LITESTREAM_GRAFANA_BUCKET?"Need to set MINIO_LITESTREAM_GRAFANA_BUCKET environment variable"}
GRAFANA_DB_PATH="/var/lib/grafana/grafana.db"

# Restore the database if it does not already exist.
if [ -f $GRAFANA_DB_PATH ]; then
	echo "Database already exists, skipping restore"
else
	echo "No database found, restoring from replica if exists"
	exec /litestream restore -v -if-replica-exists -o $GRAFANA_DB_PATH "${MINIO_LITESTREAM_ENDPOINT}/${MINIO_LITESTREAM_GRAFANA_BUCKET}/grafana.db"
fi
```
* `I use minio as a s3 compatible backup storage. Any s3 storage can be used`


To start grafana using s6-overlay the run file would have following as we are using grafana-oss container
`/etc/s6-overlay/s6-rc.d/grafana/run`
```
#!/command/with-contenv bash

exec /run.sh
```
`/etc/s6-overlay/s6-rc.d/litestream/run`
```bash
exec /litestream replicate
```

The finish script is required for any cleanups or graceful shutdown when the container is turned off and can be defined in any longrun application that requires it. For us grafana being the main app based on which we run litestream the finish script is to be added there. Here I use the default script defined by s6-overlay repo [README](https://github.com/just-containers/s6-overlay#executing-initialization-and-finalization-tasks)

`/etc/s6-overlay/s6-rc.d/grafana/finish`
```bash
#!/command/execlineb -S0

foreground { redirfd -w 1 /run/s6-linux-init-container-results/exitcode echo 0 }
/run/s6/basedir/bin/halt
```

Last part of the setup is to define empty files in `/etc/s6-overlay/s6-rc.d/users/contents.d` for all the applications that need to run by s6-overlay. In our case it would be as such


`/etc/s6-overlay/s6-rc.d/users/contents.d/grafana`
`/etc/s6-overlay/s6-rc.d/users/contents.d/litestream`
`/etc/s6-overlay/s6-rc.d/users/contents.d/litestream-restore`


For litestream you would need to define `/etc/litestream.yml`. For backing up to s3 compatible storage a minimal config would be as such

```yaml
dbs:
  - path: /var/lib/grafana/grafana.db
    replicas:
      - type: s3
        bucket: ${MINIO_LITESTREAM_GRAFANA_BUCKET}
        path: grafana.db
        endpoint: http://${MINIO_LITESTREAM_ENDPOINT}
        force-path-style: true
        retention: 24h
        snapshot-interval: 1h
        validation-interval: 6h
```

The only part left now is how to install litestream and s6-overlay in the grafana-oss container. The grafna-oss container is hardened and uses `USER 472` which restricts many operations. What worked for me is to escalate user privileges during build stage and reset them before starting. 

```bash
FROM docker.io/grafana/grafana-oss:9.5.12-ubuntu

# Set USER to root escalating priviliges to perform installation of litestream and s6-overlay
USER root

RUN apt-get -qq update && \
  apt-get -qq install -y xz-utils \
  && rm -rf /var/libs/apt/lists/*

# https://github.com/benbjohnson/litestream-s6-example/blob/main/Dockerfile
# Download the static build of Litestream directly into the path & make it executable.
ADD https://github.com/benbjohnson/litestream/releases/download/v0.3.11/litestream-v0.3.11-linux-amd64.tar.gz /tmp/litestream.tar.gz
RUN tar -C / -xvzf /tmp/litestream.tar.gz

ARG S6_OVERLAY_VERSION="3.1.5.0"
# Download the s6-overlay for process supervision.
ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz /tmp
RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz
ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-x86_64.tar.xz /tmp
RUN tar -C / -Jxpf /tmp/s6-overlay-x86_64.tar.xz

# Copy s6 init & service definitions.
COPY etc/s6-overlay /etc/s6-overlay

# Copy Litestream configuration file.
COPY etc/litestream.yml /etc/litestream.yml

# The kill grace time is set to zero because our app handles shutdown through SIGTERM.
ENV S6_KILL_GRACETIME=0

# Sync disks is enabled so that data is properly flushed.
ENV S6_SYNC_DISKS=1

# Reset USER to 472 to reset the escalated privileges
USER 472

# # Run the s6 init process on entry.
ENTRYPOINT [ "/init" ]

```

SQLite is awesome, Litestream is a game changer and s6-overlay is superb. Run apps together without external dependencies and making them resilient couldn't be simpler.

Looking to use this for few more sqlite based apps and have the confidence of production readiness.