Automating Vaultwarden Updates with a Self-Hosted CI/CD Pipeline
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 ;)