One command on your server, then one link to click. Roughly five minutes, most of it waiting for a download.
Everything below is also in the install repository, alongside the scripts themselves. That repository is public and its scripts are linted and the compose file validated on every change — you are about to run a shell script on your own machine, and that should be checkable rather than a claim.
Self-hosted ArmoryHub is a beta product. Same application, same encryption as the hosted version — but younger, running on hardware we have never seen. Expect rough edges, and keep a backup you have actually tested.
It is free during the beta: no licence key, no activation, nothing in the instance that phones home to check one. When licensing does arrive, any instance running a version below 1.0 is grandfathered and stays free permanently. More on that, including how to pay something if you want to.
A machine that stays on, running Linux. A NAS, a mini PC, an old laptop or a VM is plenty. Both 64-bit Intel/AMD (amd64) and 64-bit ARM (arm64) are supported, so a Raspberry Pi 4 or 5 works.
| Minimum | Comfortable | |
|---|---|---|
| Memory | 1 GB free | 2 GB free |
| Disk | 3 GB, plus room for photos | 10 GB+ |
| CPU | any 64-bit dual core | — |
The install downloads roughly 150 MB. Photos are the only thing that grows much over time — budget for your existing photo library, plus room for backups, which keep 7 daily and 4 weekly copies.
You also need:
sudo. Older Compose cannot read the setup scripts embedded in the compose file and fails with a confusing error about configs.You do not need a domain name, a static IP, port forwarding, any firewall changes, or root access beyond installing Docker. Nothing is exposed to the internet.
hostProxmox, and most other hypervisors, default to a generic emulated CPU — kvm64 or qemu64 — which hides the real processor's AES-NI and AVX2 instructions from the guest. Those are exactly the instructions HTTPS and password hashing use, so the default quietly forces both onto slow software fallbacks and makes signing in and loading pages sluggish for no benefit at all.
host<cpu mode='host-passthrough'/> in the domain XMLYou can change this on a VM you have already built: shut it down, change the type, start it again. Nothing inside ArmoryHub needs touching. The only thing pass-through costs you is live-migrating that VM to a machine with a different CPU, which is not something a single home instance does.
Docker and Compose must both work as your normal user, without sudo:
docker ps docker compose version
If docker ps gives a permission error on /var/run/docker.sock, your user is not in the docker group. This is the single most common way a self-host install stalls.
sudo usermod -aG docker $USER
Then log out and back in. Group membership only applies to new login sessions, so until you do, docker keeps failing and it looks like the install did not work. Check it took with docker run hello-world. On Debian you also need curl, which is not installed by default:
sudo apt-get update && sudo apt-get install -y curl
There is an optional script that does the whole lot in one run: system update, sudo, curl, ca-certificates and SSH, Docker Engine with the Compose plugin, and both group memberships.
It is optional, and nothing runs it for you. Installing ArmoryHub never invokes it. It is a convenience for people who would rather not assemble the steps themselves, and skipping it is a perfectly normal way to install. It must run as root, which is exactly why you should read it first — bootstrap-debian.sh is about a hundred lines and every step is commented.
Becoming root is the one place the two distributions differ. Debian sets a root password during installation, so su - works. Ubuntu locks the root account, so su - there rejects your password no matter what you type — use sudo -i instead.
su - # Debian: the root password you set sudo -i # Ubuntu: your own password
Then, identically on both:
apt-get update && apt-get install -y curl # Debian does not ship curl curl -fsSL https://raw.githubusercontent.com/marcjwrigg/armoryhub-selfhost-install/main/bootstrap-debian.sh -o bootstrap-debian.sh less bootstrap-debian.sh # read it before running it bash bootstrap-debian.sh <your-username>
Pass your everyday login name, not root — that is the account that gets the group memberships, and the account you should install ArmoryHub as. It is downloaded and read, then run as a separate deliberate step, rather than piped straight from the internet into a root shell.
When it finishes, log out completely and back in. exit on its own is not enough: the session underneath your root shell started before your user joined the groups, so it has not got them either. Leave the root shell and log out of your own session — over SSH, close the connection — then log back in and check with docker run hello-world.
It is safe to re-run and skips Docker if Docker is already installed. It works unchanged on Ubuntu and the other Debian derivatives, because it keys off apt-get rather than checking for a particular distribution. On anything else, install Docker via docs.docker.com and add yourself to the docker group by hand.
Browsers only allow the encryption ArmoryHub depends on over HTTPS, or on localhost. Reach the app by IP over plain http:// and you will be able to sign in and then fail to unlock your data — everything looks fine until the moment it doesn't. So the instance needs a real certificate, and Tailscale is by far the easiest way to get one.
If you have not used it before: Tailscale is a private network that links your own devices together, wherever they are. ArmoryHub uses it for two things — reaching your instance from your phone or laptop without exposing anything to the internet, and getting a real HTTPS certificate for it. It is free for personal use.
Create an account, then turn on two toggles that are free on every plan and off by default:
Everything installs correctly, then fails with your Tailscale account does not support getting TLS certs — which looks far worse than it is. Nothing is broken and nothing needs reinstalling; turn the toggle on and restart the Tailscale service.
Install the Tailscale app on every device you want to use ArmoryHub from — your phone, your laptop, anything else — signed in to the same account. Your instance lives on that private network, so its address only resolves on devices that have joined it. Nothing loads without it, which catches people who set the server up and then try to open it on a phone that is not connected yet.
Prefer your own certificates and reverse proxy? You can skip Tailscale entirely — see reverse proxies and your own domain.
On the server, as your normal user — not root:
curl -fsSL https://armoryhub.app/install.sh | sh
It checks Docker, generates every password and secret for you, pulls the image, starts everything including HTTPS, and finishes by printing a link to approve the machine on your Tailscale network. Nothing to edit and no keys to generate. That is the whole install: Postgres, the app, nightly backups, Tailscale and a real auto-renewing certificate, configured and running.
If you would rather read it before running it, the script is plain POSIX shell with no dependencies and published in full. Watch it come up with docker compose logs -f app: it applies the database schema and checks its own configuration before serving anything, and refuses to start rather than start wrong, so any failure is explained in that log.
mkdir armoryhub && cd armoryhub curl -fsSL https://raw.githubusercontent.com/marcjwrigg/armoryhub-selfhost-install/main/docker-compose.yml -o docker-compose.yml curl -fsSL https://raw.githubusercontent.com/marcjwrigg/armoryhub-selfhost-install/main/env.example -o .env chmod 600 .env # replace every CHANGE_ME — openssl rand -base64 32 # (and 48 for AUTH_JWT_SECRET) docker compose up -d
Environment variables go after the pipe, so they reach the shell running the script rather than curl:
# Install somewhere other than ~/armoryhub curl -fsSL https://armoryhub.app/install.sh | ARMORYHUB_DIR=/mnt/cache/appdata/armoryhub sh # Choose the name on your tailnet (default: armoryhub) curl -fsSL https://armoryhub.app/install.sh | ARMORYHUB_TS_HOSTNAME=armory sh
This is the one that bites on NAS hardware. The database lives in ./data/postgres beside the compose file, and Postgres on a FUSE mount or a network share risks locking errors and corruption. On Unraid that means installing to a cache-backed path: /mnt/user/... goes through shfs, which is FUSE. The same applies to NFS and SMB. ARMORYHUB_DIR is how you move it.
Tailscale certificates are rate-limited per hostname — five a week. If you have reinstalled several times and HTTPS has stopped working, a fresh ARMORYHUB_TS_HOSTNAME sidesteps a limit you have already hit.
For an unattended install, put a reusable, non-ephemeral TS_AUTHKEY in .env before running, and it registers without waiting for anyone to click a link.
Click the link the installer prints and sign in. That adds this machine to your tailnet, and the certificate is issued automatically. If the link expires before you use it, get a fresh one:
docker compose exec tailscale tailscale status
Your instance now lives at:
https://armoryhub.<your-tailnet>.ts.net
Bookmark that. It is the address to use from every device, and the only one the app can work on. That address has a real certificate, works from your phone and laptop anywhere, and is reachable only by your own devices. Nothing is exposed to the internet and you changed no router settings.
If you reach the app at something like http://192.168.1.50:8477 you will be able to sign in and then fail to set a PIN — everything else will appear to work, which makes it a confusing way to fail. Use the https://…ts.net address.
If you run a dashboard like CasaOS, its “open app” button points at http://<server-ip>:8477 — that same unusable address. Ignore the button and use your bookmark. Do not try to fix it by editing the app's Web UI setting: saving that form rewrites your compose file and deletes services. See CasaOS, Portainer and Dockge.
Open your address and follow first-run setup. You will set a password to sign in, and then a PIN to decrypt your data — two different things, and the difference matters. Read the next section before you choose them.
| Protects | Required? | Recoverable? | |
|---|---|---|---|
| Password | signing in | yes | Yes |
| PIN | decrypting your data | always | No. Never. |
| Passphrase | the copy of your key held on this server | optional | No. Never. |
Reset a forgotten password:
armoryhub password
Your PIN decrypts your data, and nobody can reset or recover it — including us. It never leaves your device in a usable form, which is what makes the encryption meaningful. Forget it and your records are permanently unreadable.
A passphrase is optional, and it is not a backup for the PIN. What it does is raise the entropy of the key material this server holds. Without one, the copy stored here is protected by a six-digit PIN, which is weak against anyone who obtains the database. With one, the server holds a copy protected by something far stronger.
If you set a passphrase, it is also required — together with your PIN — to set ArmoryHub up on another device. That follows from where the key is protected; it is not the reason for having one.
So: write your PIN down, write your passphrase down if you set one, and keep both somewhere safe and offline.
armoryhub commandThe installer sets up an armoryhub command. Use it in preference to docker compose: it reads Docker directly, so it keeps working even if a dashboard has rewritten your compose file.
armoryhub doctor # check everything and say what is wrong armoryhub status # what is running armoryhub logs [service] # follow logs, default: app armoryhub url # the address to open armoryhub backup list # what backups exist armoryhub backup now # take one immediately armoryhub backup verify latest armoryhub restore <stamp> # confirms, then stops and restarts the app armoryhub password # reset the account password armoryhub update # pull and restart
armoryhub doctor is the one to run when something is wrong. It checks the containers, whether the app answers, whether HTTPS actually works end to end, whether your compose file has been rewritten, whether the database is on storage that will corrupt it, free space, and backup age — and tells you what to do about each.
If armoryhub says “command not found”, the installer could not write to /usr/local/bin — that needs sudo, which it will not use unattended. The helper is in your install directory instead, so either run it from there or put it on your PATH once:
~/armoryhub/armoryhub doctor sudo install -m 755 ~/armoryhub/armoryhub /usr/local/bin/armoryhub
These work too, from your install directory (~/armoryhub unless you changed it). Anything with --profile will fail if a dashboard has rewritten the file.
docker compose ps # expect four: app, db, backup, tailscale docker compose logs -f app # what the app is doing, live docker compose restart app # after an .env change docker compose images app # which version is actually running docker compose exec tailscale tailscale status # is the node connected? docker compose exec tailscale tailscale serve status # is it proxying to the app? docker compose exec tailscale tailscale cert <domain> # force a certificate docker compose logs tailscale | grep -i cert # why a certificate failed docker compose --profile tools run --rm restore list docker compose --profile tools run --rm restore once docker compose --profile tools run --rm restore verify latest docker compose config --quiet # exit 0 means the compose file is valid sudo du -sh data/* # what is using space
tailscale cert is the one to reach for when the page is blank: it prints the real error, where the browser just shows you nothing.
ArmoryHub installs to your home screen or dock and then behaves like any other app — its own icon, its own window, no browser chrome. There is nothing to download from an app store.
First, on each device you want to use it from: install Tailscale from your app store, sign in to the same account, and make sure it is connected. Your instance lives on your private network, so the address only resolves while Tailscale is on. If the page will not load, that is almost always the reason.
Use Safari. Tap the Share button (the square with an arrow pointing up), scroll down, tap Add to Home Screen, then Add. Other browsers on iOS can make a shortcut, but only Safari installs it as a real app.
Use Chrome. Tap the three-dot menu, then Install app (some versions say Add to Home screen), then confirm.
In Chrome or Edge, click the install icon at the right-hand end of the address bar, then Install. If you do not see it, the three-dot menu has the same option.
In Safari, choose File → Add to Dock. In Chrome or Edge, use the install icon in the address bar as above.
Launch it from your home screen or dock afterwards. It keeps you signed in, so you only need your PIN. Remember that Tailscale has to be connected for it to load — worth knowing before you rely on it at a range with no signal.
A nightly backup runs automatically and keeps 7 daily plus 4 weekly copies.
armoryhub backup list armoryhub backup now armoryhub backup verify latest
To restore — this replaces your current data, and asks you to confirm first:
armoryhub restore <stamp>
Test a restore before you rely on one. Backups are written to ./data/backups as ordinary files, so copy them off this machine periodically — a backup on the same disk does not survive that disk failing. Their contents are already encrypted, so cloud storage is low-risk.
armoryhub update
That checks what the current release is, moves your install to it, restarts everything, and updates the armoryhub command itself. A backup is taken before any database changes are applied, and the upgrade aborts if that backup fails. It prints what it did — 0.1.6 -> 0.1.7, or already on 0.1.7 — so you can tell the difference between an upgrade and a no-op. Confirm with armoryhub doctor.
How the version is pinned, because it explains what you will see in .env. Your docker-compose.yml pins an image tag, fixed at the moment you installed, so that an install is reproducible and you can always tell what you are running. Updating sets ARMORYHUB_VERSION in .env, which the compose file reads in preference to that pin. Nothing rewrites docker-compose.yml, so local edits to it survive.
To move to a specific version, or back to one:
armoryhub update 0.1.6
Downgrades are not generally safe once a release has changed the database — check the release notes before going backwards.
The equivalent by hand is:
docker compose pull && docker compose down && docker compose up -d
On its own that re-pulls the tag already pinned in your compose file and leaves you exactly where you were, so set ARMORYHUB_VERSION in .env first if you are doing it this way. Use down then up rather than up -d on its own — some versions of Docker Compose replace a container by renaming the old one and can leave it running, quietly continuing to serve the previous version.
Every install made before 0.1.7 has an armoryhub update that cannot actually change the version: the image tag is pinned in docker-compose.yml at install time, and update pulled that same pinned tag — so it reported success, updated its own helper script, and left the app exactly where it was. If armoryhub doctor says you are on 0.1.6, this is why. The fix is in the new helper, which the old one cannot install for itself:
sudo curl -fsSL https://raw.githubusercontent.com/marcjwrigg/armoryhub-selfhost-install/main/armoryhub \ -o "$(command -v armoryhub)" && sudo chmod +x "$(command -v armoryhub)" armoryhub update
From then on armoryhub update keeps itself current and armoryhub doctor reports your version and whether it is the latest. New installs need none of this.
If you already run nginx, Caddy, Traefik or Nginx Proxy Manager and want to use your own certificates, you do not need Tailscale at all. Pass your public HTTPS address and the installer skips the entire Tailscale phase — no sidecar, no machine to approve, no certificate to wait for:
curl -fsSL https://armoryhub.app/install.sh | sh -s -- --reverse-proxy https://armory.example.com
The -s -- matters. It is what lets a piped script take arguments at all; without it the shell treats --reverse-proxy as a filename to run. If you would rather not remember that, the environment form is equivalent:
curl -fsSL https://armoryhub.app/install.sh | ARMORYHUB_REVERSE_PROXY=https://armory.example.com sh
That sets APP_URL to your address, sets APP_BIND=0.0.0.0 so a proxy outside this host can reach the app, and leaves COMPOSE_PROFILES empty so no sidecar starts. Run install.sh --help for every option. Then point your proxy at http://<this-machine>:8477 and serve it as your HTTPS address — terminate TLS at the proxy, because a plain http:// address lets you sign in and then fails to unlock your data.
APP_BIND=0.0.0.0 is the default with --reverse-proxy because it is what the common setups need — but it publishes an unencrypted login page on port 8477 to your whole network, so it is worth knowing whether you need it.
| Where your proxy runs | What to use |
|---|---|
| Same host, host network mode | --bind 127.0.0.1 — loopback is enough |
| Docker bridge network, or another machine | the default 0.0.0.0 |
| Attached to this stack's own network | --bind 127.0.0.1, and target app:3000 |
Until the proxy is actually in front of it, 0.0.0.0 means anyone on your LAN can reach that login page. Firewall the port, or use --bind 127.0.0.1 if you can.
Already installed with Tailscale and want to switch? Nothing needs reinstalling — edit .env in your install directory and restart:
cd ~/armoryhub sed -i 's|^APP_URL=.*|APP_URL=https://armory.example.com|' .env sed -i 's|^APP_BIND=.*|APP_BIND=0.0.0.0|' .env sed -i 's|^COMPOSE_PROFILES=.*|COMPOSE_PROFILES=|' .env docker compose --profile tailscale down docker compose up -d
Sign out of Tailscale first if you are done with it entirely — docker compose exec tailscale tailscale logout — or a later reinstall shows up as armoryhub-1. If your install predates --reverse-proxy it may have no APP_BIND line at all, in which case add one rather than replacing it:
grep -q '^APP_BIND=' .env || echo 'APP_BIND=0.0.0.0' >> .env
If you own a domain and want a public URL without running your own proxy, the compose file ships a Caddy profile that gets a Let's Encrypt certificate for you. In .env:
CADDY_SITE_ADDRESS=armory.example.com CADDY_ACME_EMAIL=you@example.com APP_URL=https://armory.example.com # must match CADDY_SITE_ADDRESS COMPOSE_PROFILES=caddy
APP_URL has to be the same address as CADDY_SITE_ADDRESS. There is no --caddy install flag, so everyone arriving here already has an install whose APP_URL points at a .ts.net name. Leave it pointing there and the app keeps generating links to an address Caddy is not serving.
Then point a DNS record at the machine and docker compose up -d. This needs ports 80 and 443 reachable from the internet — unlike the Tailscale route, it does put your login page on the public internet.
install.sh is a plain POSIX shell script with no dependencies, and the repository is public. If it does not do what your setup needs, clone it, edit it, and run your copy:
git clone https://github.com/marcjwrigg/armoryhub-selfhost-install.git cd armoryhub-selfhost-install # edit install.sh sh install.sh --reverse-proxy https://armory.example.com
Set ARMORYHUB_CONFIG_REPO=you/your-fork if you also want it to pull docker-compose.yml and the armoryhub helper from your fork.
The compose file works in any of them, but CasaOS rewrites it when you save anything through its settings form, including when you set the Web UI address. Measured on a real install, saving once:
docker compose can no longer start or restart Tailscale, and reports no such service: tailscale..env is not.None of that breaks a running instance, which is why it goes unnoticed. It bites later, when something needs restarting.
The safest arrangement is to run this from a terminal and, if you want a tile on the CasaOS dashboard, add it as an external link to your https://…ts.net address rather than importing the compose file. You get the icon and the click-through with CasaOS having no authority over the configuration. If you do let CasaOS manage it, keep a copy of the original compose file — CasaOS moves what it replaces to docker-compose.yml.bak, owned by root.
If you want to keep CasaOS managing the app, keep a second, pristine copy of the compose file under a name CasaOS does not touch, and use it for anything involving profiles:
cd ~/armoryhub curl -fsSL https://raw.githubusercontent.com/marcjwrigg/armoryhub-selfhost-install/main/docker-compose.yml -o compose-full.yml docker compose -f compose-full.yml --profile tools run --rm --no-deps restore list docker compose -f compose-full.yml --profile tailscale up -d
The project name comes from the directory, so this attaches to the same containers, network and data as usual — it is the same stack, just described by a file CasaOS has not edited. --no-deps stops Compose recreating your database because the two files disagree.
Your data comes over as a single encrypted archive. Nothing is re-encrypted, and no key ever travels in a form anyone could use.
.zip.Before it changes anything, it shows you which account the archive came from, when it was exported, how many records and files it contains, whether the account had a passphrase, and how many records a replace would remove. You have to tick a box acknowledging that before it will proceed.
The archive carries your master key wrapped, along with the salts needed to derive the key that unwraps it. The plaintext key is never in the file, and neither is your PIN or passphrase. That is why your existing secret still works on a completely different server: the new instance is not given your key, it is given something only your secret can open.
| Your hosted account | What you need after importing |
|---|---|
| PIN only | your hosted PIN |
| PIN and a passphrase | your passphrase, then your hosted PIN |
The second row surprises people. Enabling a passphrase deliberately removes the PIN-wrapped copy of your key from the server, so that the only copy we ever hold is protected by something far stronger than six digits. That copy is what ends up in your export — so the passphrase is the only thing that can open it.
Your records cannot be decrypted without it, on either instance. Check that you have it before you start.
Afterwards, the PIN you created during self-hosted setup is gone — it belonged to the empty account the import replaced. From then on you unlock with the secret that came across, and you can change it in Security settings once you are in. Your hosted account is untouched by any of this: keep it until you are satisfied the migration worked, and compare a few records before you rely on the new instance. The same process works in reverse if you decide to move back.
Start with armoryhub doctor. It diagnoses most of what follows and tells you what to do about it.
Ask Tailscale directly — it prints the real error where the browser shows you nothing:
docker compose exec tailscale tailscale cert <your-full-domain>
If that comes back with a 500 and your Tailscale account does not support getting TLS certs, nothing is broken and nothing needs reinstalling — HTTPS Certificates were never switched on for your tailnet. It is one toggle, free on every plan, off by default. Turn it on, then:
docker compose --profile tailscale restart tailscale
Check that docker compose ps includes tailscale. If it does not, that is why: the address resolves to nothing, which in a browser looks like a blank page with an empty console rather than an error. Tailscale is an optional service, so it only starts when Compose is told to include it — and once it has stopped for any reason, a plain up -d leaves it down.
grep COMPOSE_PROFILES .env || echo 'COMPOSE_PROFILES=tailscale' >> .env docker compose up -d
That line makes every future up -d include it. Installs created by install.sh have it already. Set it to caddy instead if you use a public domain.
Check whether another VPN is running on that device. A second VPN client will happily route around Tailscale, and the way it fails is genuinely misleading: Tailscale itself keeps working, so tailscale ping armoryhub still gets replies and everything looks reachable — while connections to port 443 quietly fail. It reads as a broken certificate or a server problem, and you can lose an evening to it. Quickest way to identify it: try the same address from your phone on mobile data. If the phone works and the desktop does not, the problem is the desktop.
You are reaching the app over plain http:// — almost always by IP address, or via a dashboard's “open app” button. Browsers withhold the cryptography the app needs from insecure origins. Use your https://…ts.net bookmark.
no such service: tailscale or no such service: restoreWhile the containers are visibly running, this means a dashboard has rewritten your compose file — see CasaOS, Portainer and Dockge.
The application checks its own configuration before it serves anything and refuses to start rather than start incorrectly, so almost every failure is explained in its log:
armoryhub logs app
# Sign out of Tailscale FIRST, or a future reinstall appears as armoryhub-1 docker compose exec tailscale tailscale logout docker compose --profile tailscale --profile tools down
That stops and removes the containers. Your data is still there, in the data directory next to docker-compose.yml, and docker compose up -d brings it all back. To delete your data as well, remove that directory yourself:
rm -rf ./data
There is no undo, and that includes your backups. Copy anything you want to keep off the machine first.
The beta is free and stays free for you — anything below 1.0 is grandfathered. But if you would rather not use it for nothing, you pick the amount.
No tier, no licence, no receipt to enter, nothing to unlock, and no follow-up. Skipping it is genuinely the expected answer.
Pay what you wantThe installer, the management helper, the compose file and the Debian bootstrap script are all public, and all of them are linted and validated on every change.