Owner's guide · written against 2.2.1

The owner's guide

Everything on one page, in four parts: putting it on a machine, the daily work of writing, letting an AI steward it, and keeping it healthy. Every command is meant to be copied as it stands, with your own domain in place of example.com. You do not need to understand any of it to follow it.

ways in
5 · one is yours
with docker
15 minutes
databases to set up
0 SQLite, in the process
memory it wants
~140 MB, idle
parts to this guide
4 · install is one
accounts to open
0 nothing to sign up for
Part one · install

Putting it on a machine

There are several ways in and one of them is yours: a rented server, a NAS in the cupboard, or the computer you are reading this on. This is the one technical part of owning a Quire Ink blog, and it happens once.

01Which way is yours

Start from what you already have rather than from what you know. The routes end in the same place, and the difference between them is only who starts the process and where the files live.

Whichever one you pick, this is the shape you end up with. It is worth twenty seconds now, because every instruction below is one of these four boxes.

one machine: a rented server, a NAS, or the computer in front of you Your domain example.com an A record points it here https A proxy Caddy or nginx holds the certificate Quire Ink one process 127.0.0.1:3000 data/ quire.db + analytics.db uploads/ your images and files
Four boxes, and two of them are folders. The app never listens to the internet itself: it answers on 127.0.0.1, which only the machine it runs on can reach, and the proxy in front is what the world talks to. Your writing lives in those two folders and nowhere else, which is why a backup is a copy of them.

02What you need first

Gather these four things before you type anything. People get stuck halfway through far more often than they get stuck at the start, and it is nearly always one of these.

A machine you can reach

Any 64-bit Linux server, on the cheapest tier. A blog uses about 140 MB of memory sitting idle, measured on the live box, so 1 GB is comfortable and 512 MB works. Disk is whatever your pictures need.

A domain name

And access to wherever its DNS lives, so you can point it at the server. Any registrar. It does not have to be a fresh one.

An authenticator app

On your phone: Google Authenticator, Aegis, 1Password, whichever you already use. Sign-in requires a six digit code and there is no way to turn that off.

Somewhere to put ten codes

Setting up two-factor prints ten recovery codes, once, and they are how you get back in when the phone is gone. A password manager or a piece of paper. Not the server.

And here is the list of things people expect to need and do not. There is no step below where any of these appears, on any route.

  • A database server. Quire Ink opens two SQLite files itself. Nothing to install, nothing to keep alive, nothing to connect to.
  • A cache, a queue, a worker. One process does all of it.
  • An S3 bucket or a CDN account. Uploads are a folder on your own disk, served from the same process.
  • An email provider, to begin with. The blog works with no mail at all. Add your own SMTP later, in the admin, when you want a newsletter.
  • A licence key, an activation, a sign-up. There is no server of ours in the path at any point, including this one.
One warning worth reading twice. Everything you write ends up in two folders on a machine you rent. Nobody backs that up for you. There is a button in the admin that hands you the entire blog as one archive, and pressing it is your job. Section 08 says where it is.

03A rented server, with Docker

The route most people want. You rent a small Linux machine, run one command, and put a certificate in front of it. Everything below is typed on the server, logged in as root over SSH.

If you have never rented one: any provider will do, and the cheapest tier they sell is enough. Pick Ubuntu 24.04 when it asks which system to install, add your SSH key when it offers, and write down the IP address it gives you at the end. That is the whole of the shopping.

  1. Point the domain at the server

    At your registrar, or wherever the domain's DNS lives, add an A record for @ with the server's IP address, and a second one for www if you want that to work too. This is the slowest step and it runs without you, so start it first and let it settle while you do the rest.

    To check it has taken, from your own laptop:

    dig +short example.com

    When that prints your server's IP address, the domain is pointed. It usually takes a few minutes.

  2. Install Docker on the server

    One command, from Docker themselves. It works on Ubuntu, Debian, Fedora and the rest without you having to know which one you picked.

    curl -fsSL https://get.docker.com | sh
  3. Start Quire Ink

    This is the install. It downloads the published image and starts the blog.

    docker run -d --name quire --restart unless-stopped \
      -p 127.0.0.1:3000:3000 \
      -e SITE_URL=https://example.com \
      -e CRON_SECRET=put-a-long-random-string-here \
      -v quire-data:/var/lib/quire/data \
      -v quire-uploads:/var/lib/quire/uploads \
      quireink/quireink:latest

    Two things in there are yours to change. SITE_URL is your own address, with https:// and no slash at the end; it is what feeds, share images and email links are built from, and section 16 explains what a wrong one quietly breaks. CRON_SECRET is any long random string you invent, and you will paste the same one in step 6; openssl rand -hex 24 prints a good one if you would rather not invent it. Nothing else needs touching: the tag :latest is the newest release, so you get the current version now and the newest one again whenever you upgrade. The blog tells you when that moment comes: once a day it asks what the newest release is (sending the version it runs and nothing about you or your readers) and shows a dot beside its version number in the admin, amber for a newer release, green for up to date. UPDATE_CHECK=0 in the environment turns the asking off.

    The two -v lines are your blog: the databases and the uploaded pictures. They live outside the container on purpose, so that upgrading it, deleting it, or breaking it does not touch your writing.

    Check it came up:

    curl -s localhost:3000/api/health
  4. Claim the blog

    docker logs quire

    A blog nobody owns yet prints the link that claims it, every time it starts:

      ┌─────────────────────────────────────────────────────────────────────────┐
      │  This blog has no owner yet. Open the link below to claim it.           │
      └─────────────────────────────────────────────────────────────────────────┘
    
      https://example.com/setup?token=…

    Open it and the rest is a browser: a username, an email, a password, then the QR code for the authenticator app on your phone and ten recovery codes, shown once. The codes are how you get back into your own blog when the phone is gone, so put them where you will still have them in two years.

    The token lives in memory, so restarting the container mints a new one and the line already in the log stops being a secret. /setup answers 404 the moment the account exists. If you would rather use the shell, docker exec quire bun run user create --username you --email you@example.com makes the same account — it asks for a password and prints nothing else, because two-factor enrolment happens in the browser at first sign-in either way.

  5. Put a certificate in front

    The app is running but only the server itself can reach it. Something has to hold the certificate and pass traffic through. Pick one of these two; they do the same job.

    Caddy fewer moving parts

    Caddy gets and renews the certificate on its own, with nothing to schedule and nothing to remember.

    Or skip this whole step. The repository ships docker-compose.caddy.yml, which is the blog and Caddy together: set SITE_URL and CADDY_DOMAIN in .env, run docker compose -f docker-compose.caddy.yml up -d, and the site is on https with the same headers as below. Use the steps here instead when something already holds ports 80 and 443 on that machine.

    Installing it yourself:

    apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
    curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
    curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
    apt update && apt install -y caddy

    Then put this in /etc/caddy/Caddyfile, replacing everything already in it:

    example.com {
      reverse_proxy 127.0.0.1:3000
      request_body {
        max_size 64MB
      }
      header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        Referrer-Policy "strict-origin-when-cross-origin"
      }
    }
    systemctl reload caddy

    Open https://example.com. The first visit is slow by a second or two while Caddy fetches the certificate, and after that it is your blog.

    nginx what the docs use

    If you already run nginx, or you would rather use what the project's own guide documents, install it with certbot and copy the server block from the self-hosting guide. It carries a full set of security headers, including a content security policy the app is tested against.

    apt install -y nginx certbot python3-certbot-nginx
    certbot --nginx -d example.com

    Two lines in that block are worth knowing about. client_max_body_size has to be at least as large as the biggest picture you will ever upload, because nginx refuses the request before the app sees it and the browser's error does not say so. And the proxy passes to http://127.0.0.1:3000, which is exactly where the container published its port.

  6. There is no cron step

    This used to be the step people skipped, and skipping it meant a post scheduled for Tuesday was still invisible on Wednesday. The blog keeps its own clock now. Every minute it brings due posts through the caches in front of them; every hour it finishes the smaller copies of your images, clears out old sessions and takes its snapshot. Nothing to add, nothing to remember.

    If you would rather drive it from your own scheduler, set CRON_INTERNAL=0 in the environment and call /api/cron yourself; the guide has the two lines.

  7. Sign in and make it yours

    Go to https://example.com/login, sign in with the username, the password you set and the six digit code from your phone. Everything after this point is clicking: the site's name, its language, the colours and type, the shape of the front page, the menu, comments, the newsletter. None of it comes back to the command line.

    If you are moving in from WordPress, Ghost, Substack or Medium, Settings has an importer that turns every post into Markdown, writes 301s for the old URL shapes, and fetches the images into your own library while you watch the count. One job stays with you: read the list of images it could not fetch, and settle it before the old hosting lapses. The details are on the compare page.

  8. Upgrading later

    Two commands, and your content is untouched because it lives in those two volumes:

    docker pull quireink/quireink:latest
    docker rm -f quire && docker run -d --name quire --restart unless-stopped \
      -p 127.0.0.1:3000:3000 \
      -e SITE_URL=https://example.com \
      -e CRON_SECRET=put-a-long-random-string-here \
      -v quire-data:/var/lib/quire/data \
      -v quire-uploads:/var/lib/quire/uploads \
      quireink/quireink:latest

    Retyping that at every upgrade is tedious, so if you expect to do it more than twice, write it down once instead. Put this in /root/quireink/docker-compose.yml:

    services:
      quire:
        image: quireink/quireink:latest
        restart: unless-stopped
        ports:
          - "127.0.0.1:3000:3000"
        environment:
          SITE_URL: https://example.com
          CRON_SECRET: put-a-long-random-string-here
        volumes:
          - quire-data:/var/lib/quire/data
          - quire-uploads:/var/lib/quire/uploads
    
    volumes:
      quire-data:
      quire-uploads:

    Then starting is docker compose up -d from that directory, and every future upgrade is:

    docker compose pull && docker compose up -d
    Take a backup before an upgrade anyway. The database updates its own shape at boot, inside a transaction, so there is no migration command to run and nothing to get wrong. That is not a reason to skip the button in the admin.

04On a NAS

A Synology, QNAP or Unraid box is already on, already has disks you trust, and already runs its own backup job. It is a good home for a blog. The recipe is the one above with two differences, and both of them are about who owns the files.

Use real folders, not Docker volumes. A named volume lives somewhere inside Docker's own storage, where your NAS backup job cannot see it. Point the two mounts at folders you made yourself and the box backs your blog up along with everything else.

Then tell the container who owns those folders. This is the one thing that catches people. A folder created by the NAS belongs to a NAS user, the container starts as somebody else, and the app cannot write. Measured on 2026-08-21, that failure was not subtle: the container died at boot with a SQLITE_CANTOPEN and a stack trace. The image now takes two numbers, adopts the folders once on first boot, and drops to that user before the app starts, so the app itself never runs as root.

Synology

  1. Make the folders

    In File Station, inside the shared folder docker, create quireink, and inside it data and uploads.

  2. Find the two numbers

    Control Panel → User & Group, open the user that owns those folders. The first non-admin user on a Synology is usually 1026, and the group users is 100. If you have SSH turned on, id yourname prints both and settles it.

  3. Create the project

    Open Container Manager, go to Project, and choose Create. Point it at the quireink folder, pick Create docker-compose.yml, and paste this in:

    services:
      quire:
        image: quireink/quireink:latest
        restart: unless-stopped
        ports:
          - "127.0.0.1:3000:3000"
        environment:
          SITE_URL: https://blog.example.com
          PUID: 1026
          PGID: 100
        volumes:
          - /volume1/docker/quireink/data:/var/lib/quire/data
          - /volume1/docker/quireink/uploads:/var/lib/quire/uploads

    Start it. The first boot prints one entrypoint: adopting line per folder and later boots print none, because the check is a single look at the folder rather than a walk through it.

    The port is bound to the NAS itself rather than to your network, so until step 5 the only thing that can reach the blog is the box it runs on. That is deliberate: it means a mistake here cannot quietly put your NAS on the internet.

  4. Claim the blog

    In Container Manager, open the running container and go to Log. Near the top of the first boot is a boxed line with a /setup address in it. Open that address in a browser.

    That is the whole account step, and it is a log panel rather than a terminal on purpose: username, email, password, then the QR code and ten recovery codes, once. Restarting the container mints a new link and voids the old one.

  5. Give it a certificate

    DSM has this built in. Control Panel → Login Portal → Advanced → Reverse Proxy: source is blog.example.com over HTTPS, destination is localhost port 3000. Then Control Panel → Security → Certificate to get a Let's Encrypt certificate for that name and assign it to the proxy rule.

QNAP and Unraid

Same compose file, different button. On QNAP it is Container Station → Applications → Create, and the folders live under /share/Container/ rather than /volume1/docker/. On Unraid, add a container from Docker Hub or use the Compose Manager plugin; the numbers there are almost always PUID: 99 and PGID: 100, which is the nobody:users pair Unraid gives its shares.

Think before you open a NAS to the internet. The box holds your photographs and your documents as well as your blog, and a port forward exposes the whole machine's front door, not just the blog. The safer shape is a tunnel: Cloudflare Tunnel or Tailscale Funnel, both of which reach out from inside your house instead of opening a way in. Then SITE_URL is the public address the tunnel gives you, and there is nothing to forward on the router at all.

05On your own computer

Worth doing before you rent anything. In five minutes you have the real thing running on your laptop, with your own writing in it, and if you decide against it you throw the folder away and nothing is left behind.

This is for trying it, not for publishing. Your machine sleeps, your home address changes when the router restarts, and your neighbours' broadband is not a data centre. Write on it, decide you like it, then move to section 03 and take your work with you; the move is a copy of two folders, not a migration.

With Docker Desktop Mac · Windows · Linux

Install Docker Desktop, open a terminal, and run one command. Note the address here is localhost, because that is genuinely what this blog's address is.

docker run -d --name quire -p 127.0.0.1:3000:3000 \
  -e SITE_URL=http://localhost:3000 \
  -v quire-data:/var/lib/quire/data \
  -v quire-uploads:/var/lib/quire/uploads \
  quireink/quireink:latest
docker logs quire

The log prints a /setup link; open it and make the account in the browser. On a machine with no public address the two-factor screen offers “Set this up later”, because before anybody has enrolled there is no second factor to get past — whoever reaches that screen with the password would simply enrol their own phone. Give the blog a real address and the offer is gone at the next sign-in. The port is bound to your own machine, so nothing else on the cafe wifi can reach it. When you are done with it:

docker rm -f quire
docker volume rm quire-data quire-uploads   # only if you want the writing gone too

With Bun, no Docker Mac · Linux

Closer to the metal, and the same thing a server runs. You need Bun 1.3 or newer, and nothing else.

curl -fsSL https://bun.sh/install | bash
git clone https://github.com/joiha-steven/quireink.git && cd quireink
bun install
bun run build:assets && bun run build:admin
DATA_DIR=./data SITE_URL=http://localhost:3000 bun src/index.ts

The last command prints a /setup link in the terminal it is running in. Open it and make the account there.

Those two build lines are not optional: they produce the small scripts the reading page uses and the admin itself, both read from disk when the app starts. Your blog is then the data and uploads folders inside the checkout, and deleting the folder deletes the blog.

On Windows, take the Docker route above, or install WSL2 and follow this one inside it. The project is developed and tested on Linux and macOS, and a Windows shell is a road nobody has walked here.

06From source, with systemd

No container runtime, nothing between you and the code. This is what the demo and the author's own blog run, so it is the best documented route of all: the self-hosting guide walks it end to end with every command.

The shape of it, so you know what you are agreeing to before you click through:

  • A system user that owns nothing but its own two directories, one for the databases and one for the uploads.
  • Bun, a git clone, bun install, and the two build commands.
  • A .env file with five lines in it, and a systemd unit that starts bun src/index.ts.
  • nginx in front for the certificate, and git pull plus a restart to upgrade. No scheduler: the process keeps its own clock.

Running from source is the deployment, not a step towards one. There is no compiled binary to fetch: a build with everything folded into one file leaves out the native part of the image resizer, and a binary that cannot resize an image is not something to ship. The checkout on the server is the artefact, and every live instance runs exactly that.

Three things in that guide are worth reading twice, because they cost somebody a whole evening each:

  • The unit's WorkingDirectory is load-bearing. The app finds its own files relative to the checkout, so a unit that starts anywhere else starts a broken blog.
  • Give systemd the full path to bun. It has no login shell and will not find it the way your terminal does.
  • Set DATA_DIR when you run any command by hand. Without it the command opens a different, empty database in whatever directory you are standing in, and then cheerfully tells you the blog has no accounts.

07Shared hosting, honestly

If what you have is a cPanel, Plesk or DirectAdmin account, the kind that costs a few dollars a month and came with a one click WordPress button, then Quire Ink will not run on it. That is worth saying plainly rather than leaving you to discover it at step four.

The reason is not fussiness. Shared hosting is built to run PHP files that wake up when someone asks for a page and are gone a moment later. Quire Ink is a program that stays running, holds its own database files open, and answers on a port. Those are different products with the same name.

The specific walls, in the order you would hit them:

  • Nothing may stay running. Shared hosts kill long-lived processes on purpose, because one customer's runaway program is everyone else's slow afternoon.
  • The "Node.js app" tab is not this. It runs Node behind Passenger. Quire Ink runs on Bun, which is a different runtime, and there is usually no way to install one without a shell.
  • The database has to be a file the process keeps open. The MySQL your plan includes is not a substitute; nothing here talks to it.

One question settles it for any host, and their support desk can answer it in a sentence: may I install my own runtime and keep a process of my own running after I log out? If yes, you effectively have a small server, and section 06 works. If the answer is no, no amount of cleverness will change it.

What to do instead, cheapest first:

  • Rent the smallest VPS anybody sells. It is roughly what the shared plan costs, and section 03 is fifteen minutes.
  • Use a machine you already own, a NAS or an old laptop, with a tunnel doing the public side. Section 04.
  • Let someone run it for you. The licence allows paid hosting of the published version, so a person who does this for a living may sell you exactly that.

08Or hand the whole job over

Everything on this page is a job an AI agent can do end to end, and unusually for this kind of software, it really can finish. There is no OAuth client to register, no dashboard to click through, no account to open with anybody. Nothing in the install needs a human to be a human.

Rent the server, get the SSH details, open Claude Code or whichever agent you use, and say something like this:

Install Quire Ink on my server at 203.0.113.10, root login, key at ~/.ssh/id_ed25519.
The domain is example.com and it already points at that IP.

Use the published Docker image, put Caddy in front for the certificate, and create me
an owner account with the username "me". Follow the guide at
https://quireink.com/install and tell me anything it asks you to decide.

When you are done, show me the TOTP secret and the recovery codes, and confirm the
site answers over https.

Two things to know. It will show you the recovery codes, so treat that conversation as the sensitive thing it is, and move them into a password manager afterwards. And an agent with SSH to a machine can do anything on that machine, so give it a fresh server rather than the one your company runs on.

Afterwards, the same trick keeps working: connect the blog's own agent surface and it can write and publish for you, through exactly the rules you use, with access you can take away in one click. That part is part three of this guide.

Part two · day to day

Using it, day to day

From here on, everything happens in the admin, from any browser, including the one on your phone. This part is the short version; the long version ships inside the product under Admin → Help, and that copy is always right for the release you run.

09Your first five minutes

Sign in and the dashboard walks you through a short checklist: name the site, pick a look, write the about page, publish something. The same five steps sit at the top of the built-in Help page. Nothing on that list needs the command line, and nothing you pick is permanent.

10Writing

The editor is a real writing surface over Markdown. Paste a whole Markdown article and it becomes a post; drop an image in and it is cut for every screen size; tables, footnotes, mathematics and code all round-trip. It saves as you type and keeps three versions of everything.

A post can wait: schedule it for a day you choose and it publishes itself, tells your subscribers, and purges the cache, all without you being awake. Writing in parts? Put posts in a series and every part links the others. And a draft has a preview link you can hand to one person before the world sees it.

11The look

Six colour palettes drawn separately for light and dark, four reading faces or one you upload, book mode, the pen marks you see on this page. Every one of these is a setting, not a stylesheet: change it and the whole site moves together, because not one size or colour is written into the pages themselves.

12Your readers

The newsletter runs on your own SMTP: sign-ups get a confirmation email, an issue goes out when you publish, and nobody’s address ever sits with a third party. Comments are one switch in Settings, moderated from the admin, with every delete going to a trash you can undo. And analytics is built in with no cookies and no banner: who read what, how far down they got, where they came from, recorded in your own database and nowhere else.

Readers can also lift a line out of a post: highlighting any sentence raises a small Copy quote control, and what it copies is the sentence plus a link that opens the post scrolled to exactly that sentence. It sends nothing anywhere and needs no account from anybody.

And if a reader prints one of your posts, or saves it as a PDF, they get the essay rather than a photograph of a web page: the buttons, the progress bar, the comment thread and the footer all stay off the paper, headings are not stranded at the foot of a page, tables and code are not cut in half, and any link that leaves your site prints its address. Nothing to switch on.

Part three · the steward

Let an AI steward it

This is the part other platforms bolt on. Quire Ink ships with a working MCP server, so the assistant you already use, Claude or any MCP client, can operate the blog through exactly the rules you do.

13Connect an assistant

Turn it on under Settings → Connections and mint a token: it is shown once, stored hashed, and dies on its own in 180 days. Point the assistant at https://example.com/api/mcp with that token, or let an OAuth connector walk you through it. From then on you talk to your blog in sentences, and every action it takes goes through the same code, the same rules and the same activity log as your own.

14What to ask it

It reads as well as writes, so the useful prompts are the ones a good steward answers:

How did my blog do this week compared to last? What should I write next?
Draft the next newsletter issue from my three latest posts and leave it
as a draft. Then send me a test so I can see it in email.
Recompose my front page around what people actually read this month.
It's autumn. Switch the blog to the sepia palette with Literata.

It can also sweep comments for spam (into the trash, never gone), search your whole archive drafts included, reply to a reader under your name when you ask it to, and take a backup snapshot before anything big. The agent cookbook is a page of prompts like these, kept with the product’s own documentation.

And if you have no MCP client at all: plug an API key (Anthropic, OpenAI or Gemini) into Settings → AI and the admin grows an Assistant page that answers the same prompts through the same tools, right in the sidebar. The key also puts the model to work on its own: alt text written for every uploaded image, an excerpt written when you publish without one, spam comments held in the trash for review.

15Where the lines are

The limits are part of the design, not a promise. The agent never sees your subscribers’ addresses or your commenters’ identities; it can only pick looks from the same curated palettes you would pick from, never free-form colour, because an agent has no eyes; its deletes go to the trash, never past it; and the button that sends the real newsletter to your readers is not reachable by an agent at all, because an email cannot be unsent. Everything it does lands in the activity log under its own name, and deleting its token in the admin ends it mid-sentence.

Part four · care

Keeping it healthy

Two short lists: the mistakes that leave a blog looking fine while something quietly breaks, and what to look at when something visibly does.

16The four things people forget

Every one of these leaves a blog that looks perfectly fine on the screen in front of you. That is exactly why they get missed.

The address it thinks it has

If SITE_URL is not set, the app does not guess it from the request, on purpose, and everything it builds says http://localhost:3000 instead: the feed, the sitemap, the share cards, every link in every newsletter. Readers see nothing wrong, because pages link to each other by path. The only things that notice are search engines and mail programs, which is the worst possible audience for a silent mistake. There is a warning line at boot and a hint under Settings → Search & URLs.

It is not taken from the request on purpose: the page cache is keyed by path, so one visit carrying a forged hostname would poison what everyone else is served.

The tick

The process keeps its own clock: due posts are brought through the caches every minute, and the hourly pass finishes image variants, clears expired sessions and writes the snapshots below. Nothing to schedule, which is the point. If you prefer your own scheduler, CRON_INTERNAL=0 turns the internal one off and /api/cron does the same two jobs on your cadence.

The backups you were going to take

There are three, and they answer different questions.

WhatWhere it landsThe question it answers
Exportyour own machine"give me a copy I hold". One button in Settings → System → Backups: both databases and every upload, streamed to your browser
Snapshotsthe same server"I broke something an hour ago". Written by the hourly tick on a schedule you set in the admin
Off the serversomewhere else entirely"the machine is gone". A script beside the process; the one that matters when it matters

The first two live on the same disk as the blog. They survive a bad edit, a bad import and a bad delete. They do not survive the disk. The backup guide covers getting a copy off the machine on a schedule.

The ten codes

Printed once, on the screen that enrolled your authenticator. Sign-in needs the code from your phone every time on a blog with a public address, and no support desk exists to let you back in. There is no second account to fall back on — the software refuses to make one, by design. Which is the real point: these ten lines are worth the two minutes it takes to put them somewhere you will still have them.

17Getting back in

There is one account and no “forgot password” email, because there is nobody to send it to and no address to trust. What there is instead is the machine: if you can reach a shell on it, you can put yourself back in. That is the same proof the install itself rests on.

You have forgotten the password

One command, run on the server. It asks for the new password on the terminal and never takes it as an argument, so it does not land in your shell history or in anybody's process list.

docker exec -it quire bun run user set-password --username you

From source, with systemd, it is the same command with the data directory named:

sudo -u quire bash -lc 'cd /home/quire/app && DATA_DIR=/var/lib/quire/data bun run user set-password --username you'

This one does need an interactive terminal — hence -it — because it is reading a password without echoing it. It is the one step on this page that a NAS log panel cannot do for you; a NAS gives you the container's Terminal tab instead. Your two-factor enrolment is untouched, so the next sign-in is the new password plus the same code from the same phone.

Forgotten the username too? bun run user list prints it, along with whether two-factor is enrolled.

You have lost the phone

This is what the ten recovery codes are for. On the two-factor screen, choose to use a recovery code instead and type one of them; each works once. Then enrol a new authenticator, which issues a fresh ten and voids the old ones.

You have lost the phone and the codes

Then the last door is the machine, and it opens the same way the install did: whoever can run a command on the box owns the box. There is no command for this — you clear the enrolment in the database yourself, and the next sign-in offers a fresh QR code and a fresh ten. Your posts, pages, uploads and settings are not touched; the only thing that changes is which authenticator the blog trusts.

Take a copy first. Copy the whole data folder, not the .db file on its own — SQLite runs in WAL mode here, so a write that happened seconds ago is still sitting in the -wal file beside it. Measured: a copy of just quire.db, taken right after an account was created, came out 4 KB with no tables in it at all. It is named like a backup and contains nothing.

docker cp quire:/var/lib/quire/data ./quire-data-backup

Then clear the enrolment. sqlite3 is deliberately not in the image — there is nothing in it that the blog does not need — so this uses the SQLite that ships inside Bun, which is already there:

docker exec quire bun -e "import{Database}from'bun:sqlite';new Database('/var/lib/quire/data/quire.db').run(\"update users set totp_secret = null, totp_last_step = null\")"

From source, with systemd, it is the same statement against the same file:

cp -a /var/lib/quire/data ~/quire-data-backup
sudo -u quire /home/quire/.bun/bin/bun -e "import{Database}from'bun:sqlite';new Database('/var/lib/quire/data/quire.db').run(\"update users set totp_secret = null, totp_last_step = null\")"

Nothing needs restarting. Go to /login, sign in with your password, and the two-factor screen will be the enrolment screen again: a new QR code, and ten new recovery codes shown once. The old codes stop working the moment the new ones are issued. Put these ones somewhere you will still have them.

18When something is wrong

Start by reading what the process itself says. It is nearly always a complete sentence about what it could not do.

docker

docker logs --tail 50 quire

systemd

journalctl -u quire -n 50 --no-pager

Then the list below, which is every failure that has actually happened to somebody, in rough order of how often.

symptom → cause → what to do
What you seeWhat it isThe fix
The feed, the share cards and the newsletter links all say localhost:3000 SITE_URL was never set, and it is deliberately not guessed Set it to your real address and restart. Section 08
The container starts and stops again; the log ends with SQLITE_CANTOPEN A folder mounted from the host belongs to a different user than the one inside Set PUID and PGID to whoever owns the folders. Section 04
Creating a user says the blog has no accounts, or you sign in and your posts are gone The command opened a different, empty database in whatever directory you were standing in Put DATA_DIR=/path/to/data in front of any command you run by hand
A post scheduled for Tuesday is still missing from the front page on Wednesday The clock is off, or something else is calling the shots: check the log at boot for clock off Unset CRON_INTERNAL, or make sure your own scheduler is calling /api/cron
Small pictures upload, a big one fails with nothing useful on screen The proxy refused the request before the app saw it Raise client_max_body_size in nginx, or max_size in Caddy, and MAX_UPLOAD_MB to match
The proxy answers 502 Nothing is listening where the proxy is looking docker ps to check it is up. From source, the app listens on 127.0.0.1 by default, which is right when the proxy is on the same machine and wrong when it is not
You publish, and the site keeps showing the old page A CDN rule is forcing its own cache time on HTML Let it obey the cache-control the app already sends. To check what is true, ask the server directly: curl -s localhost:3000/
You moved the blog to another hostname and it signed you out The session cookie is tied to one hostname by design Sign in again. Nothing is broken and nothing was lost
Analytics shows every reader as the same visitor The proxy in front reaches the app over a public address, so every request looks like it came from the proxy Set TRUST_PROXY=1, and only in that case. On a server the internet can reach directly it makes a forgeable header believable again

If none of those is it, the self-hosting guide goes deeper on every one, and the issue tracker is open.