Docker image
Mode 1A public image, or one from a private registry whose credentials live in Settings. An image reference and an internal port is the whole form.
Reference
Quasar is small on purpose, which makes this list finite. It is the whole of it — not the highlights.
Getting code onto the box.
Three ways in, and a set of behaviours designed so that the file already in your repo is the file that runs.
A public image, or one from a private registry whose credentials live in Settings. An image reference and an internal port is the whole form.
A repository carrying a docker-compose.yml or a Dockerfile. Quasar clones it, works out which one describes the application, and builds. Private repositories authenticate with a scoped token.
Paste a compose file directly. Quasar stores it under the app and runs the stack from there.
A repo with a compose file at its root is deployed as a stack — a Dockerfile next to it usually describes one service of that stack, not the whole app. The app's page states what was detected and lets you override it when the compose file exists only for local development.
Host publications of ports 80 and 443 are dropped — Traefik owns those for the entire server — the front service is joined to traefik-net, and the router labels are written onto it. The result goes to a generated docker-compose.quasar.yml beside the original, on every deploy. Your repository is never modified. A file that already carries traefik.* labels is run untouched.
No service name and no image name is ever consulted. In order: whichever service published host port 80/443; else the one the rest of the stack lines up behind through depends_on; else the only one offering the app's configured port; else the only service in the file. If nothing resolves, Quasar changes nothing rather than routing your domain at random, and the Routing panel asks. YAML anchors and merge keys are flattened first, or the labels would land on a shared anchor.
Redeploy recreates the container from what is already on the server — same image, same commit — which is what applies a configuration change. Update fetches first: git pull and rebuild, docker pull, or docker compose pull.
A secret URL per app. A push from GitHub or GitLab triggers pull, rebuild and redeploy — the same path as Update.
Every deployment recorded with its source, image, duration and outcome. The last four built images per app are kept, so rolling back is one click rather than a rebuild.
The clone, the build and docker compose stream to the app's page over server-sent events, with a progress bar per step — pull, build, start, healthcheck. The panel stays afterwards, which is where you read why a build failed. Admins only: build output is quite willing to print secrets.
A stack publishing a host port another app already holds is refused when you create it, naming the app that holds it. The alternative is the second stack starting, failing to bind, and stopping — in a log nobody reads.
Self-hosted services across 15 categories — Media, Files & sync, Downloads, Notes & docs, Tasks & projects, Dashboards & monitoring and the rest — searchable and filtered by category. The form is prefilled and the secrets are generated. The public address an entry wants in its own environment (URL, BASE_URL) is derived from the subdomain and the domain, so nothing is copied by hand.
Immich, Nextcloud, Authentik, Paperless-ngx and their like arrive with their database and their cache. Each waits on the others through healthcheck and depends_on: condition, so the first deployment does not spend a minute in a restart loop.
Game servers and databases do not speak HTTP. Quasar's proxy holds :80 and :443 and routes on the Host header, which a game client never sends — so those entries publish a port of their own and are reached at the server's address, not at a subdomain. The entry says so on its card rather than leaving you to find out.
A catalogue is a YAML document — written in the dashboard, pasted, or imported from a URL and re-fetched with a button. It adds your entries and your categories to the shipped ones; an entry reusing a shipped id replaces that card rather than sitting beside it. Nothing refreshes on its own: a catalogue describes what this server will run. A document that does not pass validation is not saved, and the page lists what is wrong with it in the words it came in. Entries can also be edited one at a time in a form — the same document from both sides.
An entry may ask questions before it prefills the form — a version, a mod loader, an amount of RAM, a port. Every answer is substituted for the entry's {{VERSION}}: in its environment, its image, its compose file, and in the name and subdomain proposed, so the second server lands neither on the first one's address nor under its name. ${VERSION} is left exactly as it is — that one is compose reading the .env at run time.
Each token declares a scope — a forge (github.com), an organisation (github.com/acme), one repository, or * as a fallback — and the narrowest match wins. Comparison is segment by segment, so github.com/acme never matches github.com/acmecorp. A personal account and a work organisation keep separate tokens and are never offered each other's.
Registry logins stored for private image pulls, alongside the Git tokens and under the same encryption.
Applications that bring their own dashboard.
New in v0.4.0, and the one feature with a page of its own — a station is a whole format rather than a button. What follows is the shape of it.
A station's deploy block is the catalogue entry shape, unchanged — so parameter substitution, generated secrets, compose rewriting and host port collision refusal all apply without a line of new code. What it adds is permissions, ui, hooks and script. Pasted or imported by URL, exactly like a catalogue.
It returns data, and Quasar renders it with its own components: 19 panel types across structure, data, input and embedding. That single rule is what removes the injection surface, and what makes a station inherit all seven themes without its author thinking about it.
Eight permissions, each narrowed by name — services for exec, globs for files, exact hosts for net.external, keys for env, verbs for lifecycle. A station with no permissions block gets a runtime that can compute and return values and nothing else, and a call into a namespace that was not granted names the missing permission rather than failing as undefined. net.external in particular must name its hosts: “may reach the internet” tells an operator nothing, and is an exfiltration channel with their signature on it.
Shown in plain words on the install screen, grouped by what they let the station reach. A revision whose permissions changed does not take effect until it is re-approved — the new document is stored but held, the page shows what changed, and the station keeps running the revision you accepted in the meantime.
The script is JavaScript run by goja, in a worker the dashboard re-executes its own binary to create — holding no Docker socket, no filesystem, no network and no database handle. Every capability is a request the parent checks and performs on the worker's behalf, so an escape from the interpreter buys a process that can do nothing, and nothing survives between two calls.
10 s for a panel source, 60 s for an action, 120 s for a hook; 128 MB resident, sampled every 50 ms by the parent and killed above the ceiling. A panic, a runaway loop or an allocation storm takes the worker and nothing else — the panel says why, and every application on the server keeps running. Long actions opt out of the 60-second ceiling by running as background jobs with a live progress pane.
It sets an accent, a tint, a typeface, a radius, a density and a mark; it never sets bg, surface, text or border, so it stays legible on all seven. accent-text is computed in Go by real contrast, and ok/warn/err are not overridable — a red that means "stopped" everywhere else must not mean something else inside a station. The scope stops at the station's block, because a program that can repaint Quasar's chrome can draw a convincing login screen.
after_deploy, on_start, on_stop, on_health_fail, and actions on a schedule. A failing hook is reported in the deploy panel and the audit log and does not fail the deploy — third-party code on the critical path of a deployment is how a working site goes down for a reason nobody can find. Scheduled actions run only while the application is running.
A new revision replaces the interface, theme, script and hooks for every application using that station — fix it once, three servers get the fix — and touches neither the compose file, the image nor the environment. Every accepted revision is kept, so reverting a broken panel is one click and the application never stopped.
Settings → Stations, beside Catalogues and sharing its import machinery. A document given as a URL keeps its source and gains a re-fetch button; nothing re-fetches on its own. Nothing is compiled into the binary — a Quasar install only has the stations its operator gave it — and a document that does not pass validation is not installed, with the page listing everything wrong with it rather than the first thing.
Names, certificates, and why one is missing.
A subdomain and an internal port is the entire routing configuration. The interesting part is what happens when it does not work.
Subdomain plus internal port becomes a generated set of Traefik labels. The certificate is issued by Let's Encrypt on the first request that arrives for the name.
Any app can answer for extra hostnames — www.myblog.com alongside its own subdomain — each with its own certificate.
Every hostname an app serves reports its certificate state, and when one is missing the page says why: the name does not resolve, or it resolves somewhere other than this server. That second case — a registrar's leftover A record still pointing at shared hosting — is the usual reason an app has no HTTPS while everything looks correct.
Every certificate Traefik holds, listed next to the app that routes it. Ones nothing routes any more can be deleted; Traefik restarts, which costs a few seconds of downtime.
The knobs each app gets.
An editor in the dashboard. Values are written to the database and to apps/<id>/.env, which is passed to compose stacks as --env-file.
A container path mounted onto apps/<id>/data/ on the host, so state survives every redeploy.
Maximum CPU and memory per application, enforced through cgroups and applied to the running container — no redeploy to raise a ceiling.
A protected app's router carries a forward-auth middleware pointing at Quasar, so every request for it is a question asked here first — which is what makes a login page possible, where the box a browser draws for basic auth cannot be styled or explained. A visitor who signs in keeps a session for that app alone, for a week. The cost is a dependency: a protected app needs the dashboard up. An unprotected one carries no such middleware and never calls it.
A ceiling in requests per second, a list of addresses or CIDR blocks allowed through, and browser hardening headers — all at the proxy, in front of the application. A bare address is normalised to its /32, and a block that does not parse is refused here rather than by Traefik, which drops the whole middleware when one is malformed and so fails open.
A command run inside the container before every backup, whose output is archived as the app's dump. It is what makes a database restorable: copying a running database's files mid-write produces a snapshot that can be unrecoverable however healthy the archive looks. A command that fails fails the whole backup, loudly — one that quietly skipped is worse than none.
Start, stop, restart, redeploy, rollback and delete — and reordering, so the dashboard list is in the order you think about them.
Finding out what is going on.
The half of hosting you actually spend time in.
Streamed over server-sent events, with ANSI colours preserved and severity inferred for lines that bring none of their own. The pane follows the tail.
Search across applications from one page rather than opening each container in turn.
CPU, memory and disk gauges for the host, plus per-container statistics. A background watcher tracks state so an app that fell over is marked without you refreshing.
CPU and memory samples — server-wide and per app — stored in SQLite and drawn as 24-hour SVG sparklines rendered on the server.
A periodic probe per app, automatic restart after three consecutive failures, and an availability history kept afterwards.
A Discord- and Slack-compatible webhook for failed deployments, apps entering an error state, recoveries, and failed backups.
Host hardware, the Docker engine and Go runtime in use, the platform's own containers, every volume on the server, Docker disk usage and per-app size — the view you open when something is wrong with the box rather than with an app.
When you need to reach inside.
A real interactive shell inside the container — xterm.js over a WebSocket. No SSH hop and no docker exec typed by hand.
Commands executed inside a container through docker exec, on demand or on a fixed interval. Output and exit status are kept between runs.
Keeping the disk and the database in hand.
On demand or nightly. Each archive holds a consistent SQLite snapshot — taken with VACUUM INTO, so a database in use is still coherent — plus every app's dump, data/ and .env. Retention is configurable and archives download from the dashboard.
Every archive pushed to S3-compatible object storage: endpoint, region, bucket, an optional prefix and a key pair, with a test upload against the real endpoint before you trust it. A backup sitting only on the server shares the disk with everything it protects. The upload failing does not fail the backup — a local archive is strictly better than none — but it is audited and it notifies, so the gap is never silent.
One click from the System page: SQLite tables are re-attached, data/ and .env are put back. Redeploy afterwards.
Archives deliberately exclude the key that encrypts stored secrets, which is what makes a leaked one useless on its own — and equally what makes it useless on a rebuilt server unless you saved the key separately. The System page hands it over for exactly that, and records every time it does.
Docker usage broken out by images, containers, volumes and build cache, with size per app. The sweep prices every category before you press it, then removes what nothing claims: untagged layers, images with no container, build cache, containers a deploy left behind, empty networks. Stopped apps, their images and each app's recent git builds — the rollback targets — are spared. Orphaned volumes are offered separately, behind a checkbox, because those do not come back with a pull.
What persisted data actually holds. Two ways in: an app's Storage section lists what it has mounted — read from the container, so the volumes an image declares for itself show up too — and the System page names every volume on the server, with its owner and its size. It is the other half of the sweep above: before ticking delete 3 orphaned volumes, you can look inside them. Every folder is a real URL, so the Back button works; text previews to 256 KB, images render, anything downloads. Admins only — these files hold whatever the app wrote, secrets included.
Upload into the open folder, edit a text file, delete one — all three audited. Writes go through a temporary file renamed over the target, so an app re-reading its configuration never sees it half-written, an interrupted write leaves the previous version intact, and a symlink is replaced rather than written through. A replaced file keeps its permissions: a secret at 600 does not come back at 644 because it was edited.
App data is editable with nothing to configure. Named volumes arrive through a read-only mount, so the kernel refuses — Quasar tests that at every navigation with access(2) rather than assuming, and shows a read-only chip instead of the buttons. One uncommented line in docker-compose.yml makes them writable; a mount the container itself holds ro stays read-only whatever happens. And a file over 256 KB is not editable at all: saving it would write the displayed part over the whole file, which is refused server-side rather than merely hidden.
What it is careful about.
The dashboard reaches Docker only through tecnativa/docker-socket-proxy, restricted to the API sections it needs: containers, images, networks, build, session, grpc, volumes, info, system, exec, and POST. EXEC=1 exists for the web terminal and tasks — drop it if you use neither. SESSION=1 and GRPC=1 give access to the daemon's BuildKit; without them docker compose build starts a privileged BuildKit container per build instead.
Admin and viewer. A viewer sees every application, log, metric and audit entry and can change nothing. The last admin account cannot be demoted or deleted.
TOTP, enrolled by QR code, required at login once enabled.
Bearer tokens carrying a role of their own, for driving the platform from CI. A viewer token reads apps and host metrics; an admin token deploys and restarts.
Who did what, to what, and from which address — searchable by user, action or target, and never rewritten.
HTTP-only, Secure, SameSite=Lax session cookies; bcrypt password hashing; active sessions listed and revocable.
The host's / is mounted read-only into the dashboard: disk metrics, the certificate store, and the volume contents the storage explorer shows. Writing into Docker's own volumes needs a second mount the operator adds deliberately — it ships commented out.
A requested path is normalised before being joined — so a .. has nothing to climb — then resolved through its symbolic links and checked to be a descendant of the root it was given. It is the second half that counts: these trees are written by application containers, which are perfectly able to leave a link to / in them. Files are served attachment with nosniff, except a closed list of image types — SVG excluded, since it would run its script in the dashboard's own origin.
Looking after Quasar.
GitHub releases are checked every thirty minutes; a button appears in the top bar as soon as one is available, and installs it through an ephemeral updater container. Only the dashboard is unavailable, for a few seconds. Your applications are not touched.
From the System page, to the version this Quasar release was tested with — never the latest on Docker Hub, because Traefik is the piece that takes every site down with it if it fails to start. The pin is written to docker-compose.override.yml, so it survives a docker compose up -d run by hand and leaves the git checkout clean. If the new version does not stay up, the old one is put back automatically.
Nebula (dark, default), Marathon, Nord, Synthwave, Terminal, Paper and Solarized. Implemented as CSS custom properties and persisted in a cookie — the switch in this page's footer is the same mechanism.
Ten minutes on a 1 GB box, and you can delete it just as fast if it is not for you.
curl -sSL https://raw.githubusercontent.com/AymericChaverot/quasar/main/setup.sh | sudo bash