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.
A VPS, with Docker
The one most people want. Two commands to get it running, one more for a certificate. Works on the cheapest tier anybody sells.
Go to 03 → you have a Synology, QNAP or UnraidOn a NAS
The box is already on, already backed up, and its container app takes the same recipe. Two folders and two numbers is the whole of the difference.
Go to 04 → you just want to look at itOn your own computer
Mac, Windows or Linux, five minutes, nothing permanent. Good for deciding whether you like it before you rent anything.
Go to 05 → you would rather not run DockerFrom source, with systemd
Bun, a checkout, a service file. This is what the demo and the author's own blog run, and it is the shape the full guide documents.
Go to 06 → you have cPanel or Plesk hostingShared hosting
The honest answer is that it will not run there, and why. There is a cheap way out and it is in that section.
Go to 07 → you would rather not do any of itHand it to an agent
Give an AI agent SSH to a fresh server and let it finish the job. There is no account to open on the way, so it really can.
Go to 08 →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.
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.
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.
-
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 forwwwif 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.
-
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
-
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_URLis your own address, withhttps://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_SECRETis any long random string you invent, and you will paste the same one in step 6;openssl rand -hex 24prints a good one if you would rather not invent it. Nothing else needs touching: the tag:latestis 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=0in the environment turns the asking off.The two
-vlines 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
-
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.
/setupanswers 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.commakes 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. -
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 shipsdocker-compose.caddy.yml, which is the blog and Caddy together: setSITE_URLandCADDY_DOMAINin.env, rundocker 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_sizehas 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 tohttp://127.0.0.1:3000, which is exactly where the container published its port. -
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=0in the environment and call/api/cronyourself; the guide has the two lines. -
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.
-
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 -dfrom 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
-
Make the folders
In File Station, inside the shared folder
docker, createquireink, and inside itdataanduploads. -
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 groupusersis100. If you have SSH turned on,id yournameprints both and settles it. -
Create the project
Open Container Manager, go to Project, and choose Create. Point it at the
quireinkfolder, 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/uploadsStart it. The first boot prints one
entrypoint: adoptingline 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.
-
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
/setupaddress 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.
-
Give it a certificate
DSM has this built in. Control Panel → Login Portal → Advanced → Reverse Proxy: source is
blog.example.comover HTTPS, destination islocalhostport3000. 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.
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.
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
.envfile with five lines in it, and a systemd unit that startsbun src/index.ts. - nginx in front for the certificate, and
git pullplus 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
WorkingDirectoryis 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_DIRwhen 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.
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.
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.
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.
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.
| What | Where it lands | The question it answers |
|---|---|---|
| Export | your own machine | "give me a copy I hold". One button in Settings → System → Backups: both databases and every upload, streamed to your browser |
| Snapshots | the same server | "I broke something an hour ago". Written by the hourly tick on a schedule you set in the admin |
| Off the server | somewhere 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.
| What you see | What it is | The 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.