Running Appsmith on Azure App Service: Solving Data Persistence for Ephemeral Containers

The Problem No One Talks About

Appsmith is a fantastic open-source platform for building internal tools. Their self-hosting docs cover Docker Compose (bind mount a local folder) and Kubernetes (use a PersistentVolumeClaim). Simple enough.

But what if you’re running Appsmith on Azure App Service — a PaaS where containers are ephemeral and Docker volumes vanish on every restart?

Appsmith CE bundles MongoDB, PostgreSQL, and Redis inside the container at /appsmith-stacks/. That path is a Docker VOLUME. On Azure App Service, that volume is ephemeral. Every time your container restarts — whether from a deployment, a scale event, or a platform update — all your dashboards, data sources, queries, and user accounts are gone.

Appsmith’s documentation has zero guidance for this scenario. No Azure App Service guide. No ephemeral-container guide. Nothing for AWS App Runner, Google Cloud Run, or any other PaaS with stateless containers.

This post documents how we solved it — including the 15 approaches we tried before finding one that works.


Why You Can’t Just Mount Azure Files

The obvious first thought: mount an Azure Files share directly at /appsmith-stacks/. Azure App Service supports this natively — configure a storage mount, point it at the container path, done.

It doesn’t work. Here’s why:

MongoDB uses the WiredTiger storage engine, which requires POSIX file locking (fcntl locks). Azure Files uses the CIFS/SMB protocol, which does not support POSIX file locks. MongoDB crashes on startup with locking errors.

This isn’t documented by Appsmith or Azure. You’ll only discover it after watching MongoDB fail in the Docker logs.


The Architecture That Works

Instead of replacing the Docker volume with Azure Files, we keep the native ephemeral volume for database operations (full POSIX compliance) and use Azure Files purely as a backup/restore staging area.

┌──────────────────────────────────────────────────────────┐
│  Azure App Service Container (ephemeral)                 │
│                                                          │
│  /appsmith-stacks/        ← Docker VOLUME (fast, POSIX) │
│    ├── data/mongodb/      ← WiredTiger (needs fcntl)    │
│    ├── data/postgres/                                    │
│    ├── data/redis/                                       │
│    └── configuration/     ← Appsmith app config          │
│                                                          │
│  /mnt/azurefiles/         ← Azure Files mount (durable) │
│    ├── startup.sh         ← Our custom startup script    │
│    └── backup.tar.gz      ← Compressed data snapshot     │
└──────────────────────────────────────────────────────────┘

Lifecycle:

  1. Container starts → Appsmith entrypoint initializes fresh databases
  2. Our startup script restores data from Azure Files backup (overwrites fresh DBs with real data)
  3. Supervisord starts all services (MongoDB, PostgreSQL, Redis, backend, editor)
  4. Background daemon snapshots data to Azure Files every 5 minutes
  5. Container restarts → cycle repeats, data survives

The Startup Script

The entire solution is a single bash script stored on the Azure Files mount:

#!/bin/bash
set -e

B=/mnt/azurefiles/backup.tar.gz
S=/appsmith-stacks

# Fix 1: CWD symlink (see "Gotcha #2" below)
ln -sf /opt/appsmith/caddy-reconfigure.mjs "$S/caddy-reconfigure.mjs" 2>/dev/null || true

# Fix 2: Selective restore — only database data and configuration
if [ -f "$B" ]; then
    echo "=== Restoring Appsmith data from backup ==="
    cd "$S"
    pkill -f mongod 2>/dev/null || true
    pkill -f postgres 2>/dev/null || true
    pkill -f redis-server 2>/dev/null || true
    sleep 2
    tar xzf "$B" data/mongodb data/postgres data/redis configuration 2>/dev/null || true
    echo "=== Restore complete ==="
fi

# Fix 3: Background backup daemon
(
    sleep 300
    while true; do
        cd "$S"
        tar czf /tmp/backup.tar.gz data configuration 2>/dev/null && \
            cp -f /tmp/backup.tar.gz "$B" && rm -f /tmp/backup.tar.gz
        sleep 300
    done
) &

# Start Appsmith services
exec /usr/bin/supervisord -n

How it hooks in: Azure App Service lets you set appCommandLine (the startup command), which overrides Docker’s CMD. Appsmith’s Dockerfile uses ENTRYPOINT for its initialization (/opt/appsmith/entrypoint.sh) and CMD for supervisord. Setting appCommandLine to bash /mnt/azurefiles/startup.sh replaces only the CMD, so the entrypoint still runs first — initializing fresh databases, generating supervisord configs, and copying runtime files — then hands off to our script via exec "$@".


The Two Gotchas That Took Days to Find

Gotcha #1: You MUST Do a Selective Restore

Our first working backup/restore attempt used a full tar xzf backup.tar.gz to restore everything. The container would start, the editor would load… and then crash-loop.

Root cause: Appsmith’s entrypoint copies runtime files into /appsmith-stacks/ — including JavaScript modules, Caddy configs, and supervisor scripts. A full tar restore overwrites these runtime files with stale versions from the backup. When the entrypoint was updated (e.g., new Appsmith image version), the backup still had old files.

Fix: Extract ONLY the data directories:

tar xzf "$B" data/mongodb data/postgres data/redis configuration

This preserves all runtime files the entrypoint just placed, while restoring only the actual database data and app configuration.

Gotcha #2: The caddy-reconfigure.mjs CWD Mismatch

Even after getting the restore right, the Appsmith editor kept crash-looping with:

Error: Cannot find module '/appsmith-stacks/caddy-reconfigure.mjs'

Here’s the chain:

  1. caddy-reconfigure.mjs is baked into the Docker image at /opt/appsmith/caddy-reconfigure.mjs
  2. Appsmith’s run-caddy.sh calls it with a relative path: node caddy-reconfigure.mjs
  3. Docker’s WORKDIR is /opt/appsmith/, so this works in normal Docker
  4. Azure App Service overrides the CWD to /appsmith-stacks/ (the VOLUME path)
  5. Node.js resolves the relative path against CWD → looks for /appsmith-stacks/caddy-reconfigure.mjsMODULE_NOT_FOUND

This is completely undocumented by both Azure and Appsmith.

Fix: A single symlink:

ln -sf /opt/appsmith/caddy-reconfigure.mjs /appsmith-stacks/caddy-reconfigure.mjs

The 15 Approaches We Tried

For the morbidly curious, here’s every approach we attempted and why it failed:

# Approach Why It Failed
1 APPSMITH_STACKS_PATH env var pointed to Azure Files Appsmith CE ignores this variable
2 Azure Files mounted directly at /appsmith-stacks/ MongoDB WiredTiger needs POSIX file locking; CIFS/SMB doesn’t support it
3 Symlinks from /appsmith-stacks/ subdirs to Azure Files CIFS/SMB doesn’t support symlinks
4 sitecontainers storage mode Same CIFS limitation; doesn’t change the protocol
5 Inline bash -c "..." in appCommandLine Nested shell quoting breaks with special characters
6 Long inline script in appCommandLine Azure enforces a 1024-character limit
7 run-with-env.sh wrapper script Script exited immediately; didn’t chain to supervisord
8 Explicit supervisord -c /path/to/conf Config files are generated at runtime to /tmp/ with dynamic names
9 Full tar xzf restore of entire backup Overwrites runtime files placed by entrypoint → editor crash-loop
10 tar --include flags for selective extract GNU tar uses positional args, not --include (that’s BSD tar)
11 Restore then re-run entrypoint Double initialization corrupts database state
12 External MongoDB Atlas $57+/month; overkill for a small internal dashboard
13 Azure Blob FUSE mount Same POSIX locking issues as CIFS
14 appCommandLine pointing to script without symlink fix Editor crash-loop from CWD mismatch (caddy-reconfigure.mjs)
15 Selective tar restore + symlink + background daemon ✅ Works

Azure App Service Setup

1. Create the Storage Account and File Share

az storage account create \
  --name yourstorageaccount \
  --resource-group your-rg \
  --sku Standard_LRS

az storage share create \
  --name appsmith-stacks \
  --account-name yourstorageaccount \
  --quota 5

2. Mount Azure Files to the Web App

az webapp config storage-account add \
  --resource-group your-rg \
  --name your-webapp \
  --custom-id appsmithdata \
  --storage-type AzureFiles \
  --share-name appsmith-stacks \
  --account-name yourstorageaccount \
  --access-key "YOUR_STORAGE_KEY" \
  --mount-path /mnt/azurefiles

3. Upload the Startup Script

Upload startup.sh (shown above) to the Azure Files share root.

4. Set the Startup Command

az webapp config set \
  --resource-group your-rg \
  --name your-webapp \
  --startup-file "bash /mnt/azurefiles/startup.sh"

5. Restart and Verify

az webapp restart --resource-group your-rg --name your-webapp

Check the Docker logs for:

=== Restoring Appsmith data from backup ===
=== Restore complete ===

All services should enter RUNNING state within ~90 seconds.


Key Design Decisions

Why tar instead of rsync?
Azure Files over CIFS doesn’t handle rsync’s temp files well. A single tar czf followed by cp is atomic-enough and avoids partial-write corruption.

Why 5-minute backup intervals?
Balances data safety against I/O overhead. For an internal dashboard, losing at most 5 minutes of changes on an unexpected crash is acceptable. Adjust sleep 300 in the script to change.

Why not use Appsmith’s built-in appsmithctl backup?
The appsmithctl tool is designed for interactive, manual use inside a running container. It doesn’t support automated scheduling on the CE edition (the cron-based auto-backup is an Enterprise feature). Our background daemon is simpler and more reliable.

Why kill database processes before restore?
If the entrypoint started fresh databases before our script runs, we need to stop them before overwriting their data files. The pkill commands ensure clean file handles before tar extraction.


Applicability Beyond Appsmith

This pattern — backup to durable storage, restore on startup, daemon to keep backups fresh — works for any Docker container with embedded databases running on ephemeral PaaS:

  • N8N on Azure App Service (uses SQLite or PostgreSQL internally)
  • Gitea/Forgejo on Cloud Run (embedded SQLite)
  • Metabase on App Runner (embedded H2 database)
  • Any self-contained app that stores state in a Docker VOLUME

The key requirements:

  1. A durable file mount accessible from the container (Azure Files, EFS, GCS FUSE)
  2. A startup hook to run a script before the main process (appCommandLine, entrypoint override, init container)
  3. Selective restore to avoid clobbering runtime files the original entrypoint generates

Conclusion

Azure App Service is a great platform for running Docker containers — but its ephemeral storage model creates a real challenge for stateful applications like Appsmith. The official Appsmith docs don’t address this, and the obvious solution (mounting Azure Files at the data path) fails silently due to POSIX file locking incompatibilities.

Our solution — a 20-line bash script that selectively restores from backup, fixes a CWD mismatch, and runs a background backup daemon — has been running in production with zero data loss across multiple container restarts.


Have questions or found a better approach? Reach out at [email protected].

Leave a Reply

Your email address will not be published. Required fields are marked *