Skip to main content

Automating Vaultwarden Updates with a Self-Hosted CI/CD Pipeline

·3 mins

Written with the help of Local LLM Qwen3.5 9B Q4_K_M running on Macbook Air M4 + Hermes.

I spent about an hour to automate updating Vaultwarden, and now it’s a “lazy, it works fine” setup.

Why? #

As mentioned in the State of the Homelab 2026 report, I’m diving deeper into CI/CD and automation for my homelab. Lately, updating Vaultwarden has become a pain in the a - especially since the extension and mobile app tend to break if you don’t keep up with the latest version.

12. The Core Script: update_vaultwarden.sh #

Here’s the magic sauce: a simple Bash script that does the heavy lifting for you.

#!/bin/bash
set -e

COMPOSE_FILE="./docker-compose.yml"
CONTAINER_NAME="vaultwarden"
SERVICE_NAME="vaultwarden"
VERSION="0.1"

cd "$(dirname "$0")"

echo "Pulling latest changes from repo..."
git pull origin main

OLD_IMAGE_ID=$(docker inspect -f '{{.Image}}' "$CONTAINER_NAME" 2>/dev/null || echo "")

echo "Pulling latest image..."
docker compose -f "$COMPOSE_FILE" pull

NEW_IMAGE_ID=$(docker compose -f "$COMPOSE_FILE" images -q "$SERVICE_NAME")

if [ "$OLD_IMAGE_ID" == "$NEW_IMAGE_ID" ]; then
  echo "Already running the latest image. Nothing to do."
  exit 0
fi

echo "New image detected, updating container..."
docker compose -f "$COMPOSE_FILE" up -d

# Wait for container to start
for i in $(seq 1 10); do
  [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null)" == "true" ] && break
  sleep 2
done

if [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null)" != "true" ]; then
  echo "Container failed to reach running state."
  docker logs "$CONTAINER_NAME" --tail 50
  exit 1
fi

echo "Vaultwarden updated successfully."

The script is quite simple using super basic bash, I didn’t need any fancy stuff.

TLDR; #

  • Pulls the latest code from the repository.
  • Checks for new Docker images by comparing the current and new image IDs.
  • Pulls and deploys the latest image if changes are detected.
  • Monitors the container to ensure it starts successfully.
  • Logs errors if the update fails

2. Docker Compose Configuration #

The docker-compose.yml file defines the Vaultwarden service with:

services:
  vaultwarden:
    image: vaultwarden/server:1.37.1
    container_name: vaultwarden
    restart: unless-stopped
    user: "1001:1001"
    environment:
      - DOMAIN=${DOMAIN_URL}
      - ADMIN_TOKEN=${ADMIN_TOKEN}
      - PUSH_ENABLED=true
      - PUSH_INSTALLATION_ID=${INSTALL_ID}
      - PUSH_INSTALLATION_KEY=${INSTALL_KEY}
    volumes:
      - ./vw-data/:/data/
    ports:
      - "192.168.100.99:8000:80"

Pro Tip: Use Renovate to keep the image up to date. It’s like a personal assistant for Docker versions.

3. CI stuffs (Github actions) #

The workflow triggers the update script via SSH on a self-hosted runner. Yes, I know - this is a very bad approach. But for now, it works.

name: Vaultwarden Deployment/Update
on:
  push:
    branches:
      - main
    paths:
      - 'docker/vaultwarden-docker/docker-compose.yml'
      - 'docker/vaultwarden-docker/update_vaultwarden.sh'
      - '.github/workflows/vaultwarden.yml'

jobs:
  update_vaultwarden:
    runs-on: self-hosted
    steps:
      - name: Run deployment script
        run: |
          ssh "${{ secrets.VAULTWARDEN_HOST }}@bitwarden" "bash ${{ vars.VAULTWARDEN_UPDATE_DIR }}"

4. Final thoughts #

This setup isn’t perfect - but it’s functional, and that’s what matters. The script is simple, the Docker config is straightforward, and the CI/CD is… well, a bit of a hack. I will definetly come back to this relatively soon to improve it. Since currently we do not have a way to test if the version “works” we are assuming that everything works and pull the image directly. Part 2 will come sooner than later ;)

Author
Raif Coonjah
SysOps Consultant at Teamwork Mauritius, working across Linux systems, automation, and cloud infrastructure. Off the clock: homelab tinkering, sim racing, and gaming.