Docker image
Public, or from a private registry. Give it an image and an internal port; Quasar does the rest.
Your own platform, on your own server
Quasar turns a blank Linux box into a platform: an admin dashboard on
admin.your-domain.com, and every app you deploy on its own
subdomain with a certificate it issues itself.
curl -sSL https://raw.githubusercontent.com/AymericChaverot/quasar/main/setup.sh | sudo bash
Four questions, then a platform.
The script installs Docker if it is missing, asks for your root domain,
a Let's Encrypt email, and the admin account it should create. Then it
clones into /opt/quasar, writes the configuration, and
brings up Quasar: its router, its socket proxy and the dashboard.
Nothing to configure afterwards. The first request to
admin.your-domain.com is what triggers certificate
issuance, which is why the first load takes a few seconds and every
one after it does not.
curl -sSL .../setup.sh | sudo bash
Docker not found — installing
Root domain example.com
Let's Encrypt email ops@example.com
Admin username admin
Admin password ••••••••••••
Cloning into /opt/quasar
Writing configuration, .env (chmod 600)
Pulling ghcr.io/aymericchaverot/quasar
Starting router · socket-proxy · dashboard
Platform up in 94s
https://admin.example.com
Three ways to ship.
All three end in the same place: a container on the network, a subdomain routed to it, a certificate issued for it.
Public, or from a private registry. Give it an image and an internal port; Quasar does the rest.
A repo with a Dockerfile or a compose file. Quasar clones it, detects which one describes the app, and builds. Private repos authenticate with a scoped token.
Paste a docker-compose.yml and Quasar runs the stack — after rewriting it to sit behind the proxy.
A repo carrying a compose file at its root is run as a stack, not
built from its Dockerfile — a Dockerfile sitting next
to it usually describes just one service of that stack. The app's
page shows what was detected and lets you switch to the Dockerfile
explicitly when the compose file is only there for local
development.
Redeploy recreates the container from what is
already on the server — same image, same commit. It is what applies
a configuration change. Update fetches the new
version first: git pull and rebuild,
docker pull, or docker compose pull.
A per-app webhook URL runs the second one on every push.
Your compose file, adapted on the way in.
An ordinary docker-compose.yml — the one that runs as-is on
a laptop, with its own nginx bound to port 80 — cannot work behind a
proxy that already owns 80 and 443. Quasar rewrites it instead of asking
you to.
services:
web:
image: nginx:alpine
ports:
- "80:80"
depends_on: [api]
api:
build: .
expose: ["8000"]
services:
web:
image: nginx:alpine
ports:
- "80:80" networks: [default, traefik-net]
labels:
traefik.enable: "true"
traefik.http.routers.blog.rule: Host(`blog.example.com`)
traefik.http.routers.blog.tls.certresolver: le
traefik.http.services.blog.loadbalancer.server.port: "80" depends_on: [api]
api:
build: .
expose: ["8000"]
networks:
traefik-net: { external: true }
The rewrite is written to a docker-compose.quasar.yml
next to the original, regenerated on every deploy. A file that
already carries its own routing labels is left to describe its own
routing, and runs exactly as it is.
No service name or image name is ever consulted. In order: whoever
published host port 80/443, else whoever the rest of the stack lines
up behind via depends_on, else the only one offering the
app's configured port, else the only service in the file.
If none of those rules resolves, Quasar leaves the file alone rather than routing your domain at random, and the Routing panel asks you to pick. YAML anchors and merge keys are flattened first — otherwise the labels would land on a shared anchor.
A subdomain and a port. That is the whole configuration.
You give
blog.example.com
port 3000
Plus any extra hostnames the app should answer for —
www.myblog.com included.
Quasar
routes it, secures it
Quasar owns 80 and 443 on the box. It writes the routing for the container, sends the hostname there, and gets the certificate from Let's Encrypt on the first request that arrives for the name — then renews it for as long as the app exists.
Your app
https://blog.example.com
Reached over TLS, with whatever protections the app has configured — basic auth, resource limits, healthchecks.
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 — is the usual reason an app has no HTTPS while everything looks correct.
Every certificate Quasar holds, next to the app that routes it. The ones nothing routes any more can be deleted — Quasar reloads its router, which costs a few seconds.
Deploying is the easy half.
What you actually spend time on is the week after: reading logs, getting a shell, finding out what filled the disk. All of it is in the dashboard.
The clone, the build and docker compose scroll past as they happen, a progress bar per step. The panel stays afterwards — it is where you read why a build failed.
Streamed as they happen, ANSI colours preserved, severity inferred for lines that bring none. Searchable across every app.
A real interactive shell inside the container, in the browser. No SSH hop, no docker exec by hand.
Walk what an app actually persisted — preview it, download it, upload into it, edit a file in place without changing its permissions.
Commands run inside a container, on demand or every N minutes. Output and exit status are kept.
CPU, memory and disk for the host and per app, sampled by Quasar and drawn as 24-hour sparklines rendered server-side.
A periodic HTTP probe, automatic restart after three failures, and an availability history you can read afterwards.
Every deployment logged with its source, image, duration and result. The last four built images are kept — rollback is one click.
On demand or nightly: a consistent snapshot, each app's data/ and .env, and a database dump if you ask for one. Copied to S3-compatible storage — the copy that survives the box.
Reclaimable space is costed per category before you press anything, then swept. Stopped apps, their images and each app's rollback targets are spared.
A Discord/Slack-compatible webhook for failed deploys, apps entering error, recoveries and failed backups.
Releases are checked every 30 minutes; a button appears in the top bar. Traefik updates the same way, and puts the old version back if the new one does not stay up.
62 apps, one click each.
Search it, filter it, press the one you came for. The form arrives filled in and the secrets generated — and the public address the app expects to be told about itself is worked out from your own domain.
Immich, Nextcloud, Authentik, Paperless-ngx and the rest of the large ones arrive with their database and their cache, each waiting on the other — so the first deployment is not a restart loop.
A catalogue is a YAML document — written here, pasted, or imported from a URL. An entry may ask a question first, a version or a port, which is what makes one of them cover a fleet rather than a server.
Some applications deserve more than a start button.
A station is one YAML document that deploys an application and gives its page a control surface of its own — written by somebody who knows what running that particular service involves.
An example. A station somebody wrote for a game server — not something Quasar ships. Press the tabs.
Players online
4 / 20
Memory
2.1 / 4 GB
Build
Fabric 0.16.9
Console
Sent over RCON, inside this server's own container.
Online now
| Player | Actions | |
|---|---|---|
| Notch | op | KickBan |
| jeb_ | whitelisted | KickBan |
| dinnerbone | whitelisted | KickBan |
Re-read every 30 seconds, because the panel said so.
Last archive
world-2026-08-21.tar.zst
412 MB · 2 hours ago
A file out of the folder the station was allowed to read.
What this station has done
Installed mods
| Mod | Version | |
|---|---|---|
| Fabric API | 0.115.1 | current |
| Lithium | 0.14.3 | update |
| Simple Voice Chat | 2.5.26 | current |
api.modrinth.com — one of the three hosts this station
named, and the only reason it may reach the network at all.
The same containers, logs, storage explorer, backups and TLS as anything else on the dashboard. Remove the station and a perfectly normal application is left running.
Every privileged thing is enumerated, named — hosts, paths, services, keys — and shown to you in plain words before anything is installed. A revision that asks for more waits until you accept it.
The script runs in a disposable process holding no socket, no disk and no network, bounded in time and memory from outside. When one breaks, it is the only thing that breaks.
What it is careful about, and what it is not.
Quasar never hands the dashboard the Docker socket. It reaches Docker through a proxy of its own, opened to the API sections it needs and no others. EXEC is required by the web terminal and tasks — drop it if you use neither.
Each token declares a scope — a forge, an organisation, a single repo, or * as a fallback — and the narrowest one wins. Encrypted with the master key, never shown again, testable with a real git ls-remote.
HTTP-only, Secure, SameSite=Lax cookies; bcrypt passwords; TOTP two-factor with a QR enrolment. Sessions are listed and revocable.
Password protection is a page Quasar serves, not the grey box the browser draws. Switched on per app, with a session of that app's own — and nothing to add inside the app.
Per app, at the proxy: requests per second, an allowlist of addresses, hardening headers. Quasar checks a CIDR itself — a malformed one makes the proxy drop the rule, and fail open.
The host's / is mounted read-only: disk metrics, the certificate store, and what the volume explorer shows. Writing into Docker's own volumes stays off until you turn it on.
One thing worth knowing. A compose stack that publishes host ports other than 80/443 keeps them — a stack may legitimately want to expose a database or a game server. Those ports do not go through Quasar, and therefore bypass TLS and the app's configured protections. The Routing panel flags them rather than removing them silently.
Point a domain and a wildcard at it, open 22, 80 and 443, then run this as root.
curl -sSL https://raw.githubusercontent.com/AymericChaverot/quasar/main/setup.sh | sudo bash