Deploy with Docker Compose
Run Quaso on a Linux virtual machine (VM) with Docker Compose. The supplied files run the server, store its data in a Docker volume, and use Caddy to serve it over HTTPS.
Before you start, install git, Docker Engine and its Compose plugin. You need a public address and a
hostname you control. Allow inbound TCP ports 80 and 443; UDP port 443 is optional for HTTP/3. Keep
port 8000 private when Caddy receives public traffic. Replace translate.example.com in this guide
with your hostname.
These instructions describe the current main branch. They use its source checkout and the image
built from the same commit. For a tagged release, use the documentation in that release's checkout.
The quick start continues from deployment to your first translation files.
Five steps
Point your domain's DNS A record at the VM's IPv4 address. If you add an AAAA record, its IPv6 address must reach the same VM. Wait until your hostname resolves correctly.
Get the source and prepare the deployment files:
git clone --branch main 'https://github.com/explodi/quaso.git' cd quaso/deploy cp .env.example .env chmod 600 .env QUASO_IMAGE="explodi/quaso:sha-$(git rev-parse --short=7 HEAD)" printf '%s\n' "$QUASO_IMAGE"In
compose.yaml, replaceimage: explodi/quaso:latestwith the image printed above. Thesha-<commit>tag selects the build for that source commit;latestfollowsmainas it changes. Wait for the commit's Docker image workflow to publish successfully before pulling it. If the image is not published, wait for publication or setimage: quaso:localand rundocker compose build quasofrom this checkout. When using that local image, skip the pull command for Quaso in the next step and pull Caddy withdocker compose pull caddy.Edit
.env. SetQUASO_DOMAIN=translate.example.comandSETUP_KEYto the output ofopenssl rand -hex 16. KeepTRUST_PROXY=true, because Caddy forwards requests to Quaso. After creating the administrator, enter a Gemini API key in Settings → AI translation if you want machine translation. Without a key, you can translate and review by hand. The configuration reference describes the remaining settings.Start the containers:
docker compose pull docker compose up -d --no-buildOpen
https://translate.example.com/setup, enter your setup key, and create the first administrator. Keep the key private until setup is complete; you can then remove it from.env. Configure the project and languages in Settings, then check the server:curl --fail https://translate.example.com/healthzExpect
ok: trueandstorage: "local"in the JSON response. Next, connect your game's repository.
What runs and where data lives
Caddy obtains and renews the HTTPS certificate and proxies requests to Quaso. Quaso is one Node
application serving the API and website, on a distroless base image with no shell. Its image runs
as user 65532. The supplied Compose
file makes its root filesystem read-only. The named quaso-data volume is mounted at /data,
containing quaso.sqlite and its write-ahead log, quaso.lock, and store/, which holds the
published files and the backups. Compose prefixes volume names with the deployment directory or
project name. Caddy has separate certificate and configuration volumes. Never run
docker compose down -v against data you want to keep.
Without Compose, prepare .env with SETUP_KEY as described above, and use an existing reverse
proxy for HTTPS. Use QUASO_IMAGE from the checkout step above:
docker run -d --name quaso --restart unless-stopped \
-p 127.0.0.1:8000:8000 -v quaso-data:/data \
--env-file .env \
-e PUBLIC_URL=https://translate.example.com \
"$QUASO_IMAGE"
The loopback binding lets a proxy on this VM reach Quaso. Set TRUST_PROXY=true only when a trusted
proxy is the only route to the server. Configure HTTPS on that proxy before opening setup.
API keys
Once the instance is running, follow From your repository to every language to connect your game and run the translation workflow, or Migrate from Crowdin to bring existing translations and their proofread state.
An administrator creates a named key in Settings → API keys, and everyone signed in can create
their own on their account page. Use read for downloads and status; use upload for uploads,
imports and LLM runs. The secret is shown once. A key created in the website is limited by its
creator’s current role and assigned languages as well as its scope. Create CI upload keys with an
administrator account.
The following commands run the server's own commands inside the image, which differ from the npm CLI's. For recovery without the website, stop the server before creating a key:
docker compose stop quaso
docker compose run --rm quaso token create --name ci --scope upload
docker compose up -d quaso
docker compose exec quaso /app/quaso token create … cannot open local storage while the running
server holds its lock.
Upgrades and rollback
Read the release notes and retain a backup and the current image's exact version or digest. Test a
restore before a major upgrade. Pin the desired image version in compose.yaml, then run:
docker compose pull
docker compose up -d
docker compose logs --tail 100 quaso
curl --fail https://translate.example.com/healthz
The database migrates on startup. Before changing a supported schema, Quaso writes a store object
named backups/pre-migration-vN-to-vM-TIMESTAMP.sqlite. Those snapshots are not rotated by
scheduled retention. Migrations only move forwards. An older image cannot use a newer schema. The
1.0.0-rc.1 data format has no upgrade path.
To roll back, choose the snapshot from before the upgrade and copy it out while the container is stopped. Use the current image to extract it before changing image versions. Replace the sample key with the one recorded in the migration log. Extraction does not open the live database, so it works even when that database cannot start:
docker compose stop quaso
SNAPSHOT=backups/pre-migration-v1-to-v2-20261001T030000Z.sqlite
docker compose run --rm quaso backup "$SNAPSHOT" /data/rollback.sqlite
docker compose cp quaso:/data/rollback.sqlite ./rollback.sqlite
docker compose down
mkdir rollback
cp compose.yaml Caddyfile .env rollback/
cp rollback.sqlite rollback/
cd rollback
Edit the copied compose.yaml to use the previous image version. Restore into a new named volume
and start it:
docker compose -p quaso-rollback run --rm -v "$PWD/rollback.sqlite:/rollback.sqlite:ro" quaso restore /rollback.sqlite
docker compose -p quaso-rollback up -d
The previous deployment's volumes remain untouched; down without -v preserves them. Stopping its
Caddy also frees ports 80/443 for the replacement. Check health, sign-in and downloaded files before
resuming writes. Writes made since the snapshot are missing; retain the newer volume for recovery.
Backups and a restore drill
Scheduled SQLite copies go under the store's backups/ prefix in /data/store every hour. Copies
less than 48 hours old are kept. Older copies are reduced to the newest per UTC day, within the
window set in Settings → History & retention (30 days by default). Pre-migration copies are kept
separately. Copy backups off the VM: the volume alone does not protect against a lost VM.
To retrieve a retained copy, sign in as an administrator and download
/api/v1/backup?at=2026-10-02T00:00Z. This returns the newest copy at or before that time, as a
SQLite file usable by quaso restore. The store's physical object files include metadata and should
be copied as part of the whole volume, rather than opened as SQLite. The server command
quaso backup --at 2026-10-02T00:00Z /data/recovery.sqlite also extracts a retained copy without
starting or migrating the database. Unlike a website download, this extract preserves the stored
secrets. It refuses an existing output file. quaso backup <store-key> <file> extracts a named
scheduled or pre-migration copy.
Administrators can download SQLite or JSON backups from Settings → Backups while Quaso runs. You
can also copy the whole /data directory while Quaso is stopped. A backup download contains
accounts, password hashes and hashed API keys, so protect it like the live database. It leaves out
the Gemini and email API keys: after a restore, Settings lists them to enter again.
Practice this on a separate, empty instance:
Download a backup as
backup.sqlite. Recordquaso status --jsonand save downloaded translation files for comparison.Use the same or a newer compatible Quaso image on a separate test VM. Copy the three deployment files and
backup.sqliteto its deployment directory. Set a different hostname in.env. Ensure user 65532 can read the backup file mounted in the next step.Restore before claiming the new instance. An explicit project name gives it a new volume:
docker compose -p quaso-drill run --rm \ -v "$PWD/backup.sqlite:/backup.sqlite:ro" quaso restore /backup.sqlite docker compose -p quaso-drill up -dCheck
/healthz, sign in with an existing account, compare status and downloads, check history, and make an edit on the drill instance. Use its hostname for every CLI command.Stop the drill with
docker compose -p quaso-drill down. Record the backup date, version, results, elapsed recovery time and any manual steps. Repeat after changes to backup or deployment settings.
The server subcommand is quaso restore <file>; it accepts .sqlite, .json and .json.gz and
refuses a destination with strings or assigned roles. People sign in again because restores leave
out sessions; their passwords and API keys keep working. Older supported schemas are migrated; newer
schemas require upgrading the restoring image first. The 1.0.0-rc.1 format is not supported. If a
restore is interrupted, retry before using that destination. See operations for
cross-storage drills.
Troubleshooting
- Start with
docker compose logs --tail 100 quaso caddyanddocker compose ps. /healthzreturns JSON with the version and storage mode. Docker runs/app/quaso healthcheckagainst the local HTTP port. A successful health check does not verify a Gemini credential.- “Another Quaso server is using /data” means a process holds the operating system lock. Stop the
other instance. Do not delete
quaso.lock: the kernel releases the lock when its owner exits, including after a crash. - For a bind mount, give user/group 65532 write access to its directory. A named volume normally inherits the image's ownership. Check disk space and permissions when snapshots or startup fail.
- If HTTPS fails, check DNS, inbound ports 80/443 and Caddy's logs. Do not publish a broken AAAA record.