Restoring a Forgejo backup

Self-hosting is a fun and interesting way to learn about the technologies you use. One thing you have to take care of is to have backups of important systems. I had to upgrade forgejo from v13 to v14, but you want to be sure that works before doing it, right?

How backups are made

On my devbox I have a daily backup that creates an off-site backup of the volumes.

#!/bin/bash

# Variables
BACKUP_DIR=/mnt/backups-devbox
FOLDERS_TO_BACKUP=("forgejo")

if [ ! -d $BACKUP_DIR ]; then
  echo "Backup dir ($BACKUP_DIR) does not exist, creating."
  mkdir $BACKUP_DIR
fi

if [ ! -e $BACKUP_DIR/.canary ]; then
  echo "Canary file not found, not doing backups!"
  exit 1
fi

echo "Creating backups of PostgreSQL"

DB_USER=forgejo
CONTAINER_NAME=db

# Get current date and time for backup file
TIMESTAMP=$(date +"%F_%T")
BACKUP_FILE_FORGEJO=$BACKUP_DIR/backup_forgejo_$DB_NAME_$TIMESTAMP.sql

# Run pg_dump inside the PostgreSQL container
docker exec -t $CONTAINER_NAME pg_dump -U $DB_USER forgejo >$BACKUP_FILE_FORGEJO &&
  echo "Backup completed: $BACKUP_FILE_FORGEJO"

echo "Creating compressed tarballs of specified folders"

for FOLDER in "${FOLDERS_TO_BACKUP[@]}"; do
  if [ -d "$FOLDER" ]; then
    # Create a filename-friendly string from the path (e.g., /etc/nginx -> etc_nginx)
    FOLDER_NAME=$(echo "$FOLDER" | sed 's/^\///; s/\//_/g')
    TAR_NAME="$BACKUP_DIR/folder_${FOLDER_NAME}_$TIMESTAMP.tar.gz"
    
    echo "Backing up $FOLDER to $TAR_NAME..."
    tar -czf "$TAR_NAME" "$FOLDER"
  else
    echo "Warning: Folder $FOLDER not found, skipping."
  fi
done

echo "Backups done"

This is of course all very well, but this backup needs to be tested.

An untested backup is no backup.

Running a test setup

I am running Forgejo as a docker container on my devbox. The actual configuration is done with ansible and it sits nicely behind a reverse proxy, but for testing purposes locally this docker-compose file builds the same setup:

services:
  db:
    image: postgres:16
    container_name: db
    restart: always
    ports:
      - "5432:5432"
    environment:
      USER_UID: "1000"
      USER_GID: "1000"
      POSTGRES_USER: "forgejo"
      POSTGRES_PASSWORD: "forgejo"
      POSTGRES_DB: "forgejo"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready", "-d", "forgejo"]
      interval: 30s
      timeout: 60s
      retries: 5
      start_period: 80s
    volumes:
      - ./postgres:/var/lib/postgresql/data
    networks:
      - forgejo

  forgejo:
    image: codeberg.org/forgejo/forgejo:14
    container_name: forgejo
    restart: always
    ports:
      - "3000:3000"
      - "22:22"
    environment:
      USER_UID: "1000"
      USER_GID: "1000"
      FORGEJO__database__DB_TYPE: "postgres"
      FORGEJO__database__HOST: "db:5432"
      FORGEJO__database__NAME: "forgejo"
      FORGEJO__database__USER: "forgejo"
      FORGEJO__database__PASSWD: "forgejo"
    volumes:
      - ./forgejo:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    networks:
      - forgejo

networks:
  forgejo:
    name: forgejo

Restoring postgres

So, first I only run the postgres container so that I can re-create the database. The restore is most easily done by copying the dump into the image and restoring it:

docker cp backup_forgejo_2026-SOMEDATE.sql db:/tmp/dump.sql
docker exec -it db psql -U forgejo -d forgejo -f /tmp/dump.sql

Restoring forgejo

Then it is time to recreate the forgejo directory from the backup:

tar zxvf folder_forgejo_2026-SOMEDATE.tar.gz

Remember to run these commands in the directory of the docker-compose file.

Finally ensure everything is fine by running the forgejo docter:

docker exec -u git -ti forgejo bash
forgejo doctor check --all