Guide first-run setup through the web UI

This commit is contained in:
Nick Sweeting 2026-08-19 00:45:40 -07:00
parent 11c23281c1
commit 220e4322dc
No known key found for this signature in database
7 changed files with 85 additions and 40 deletions

View File

@ -73,7 +73,7 @@ mkdir -p ~/archivebox/data && cd ~/archivebox
curl -fsSL 'https://docker-compose.archivebox.io' > docker-compose.yml
docker compose pull
docker compose up -d --wait # initializes new collections automatically
docker compose exec archivebox archivebox manage createsuperuser # create the first Web UI user
# open http://admin.archivebox.localhost:8000 to finish setup
# docker compose run --rm archivebox add 'https://example.com'
# docker compose run --rm archivebox help
<br/>
@ -82,8 +82,8 @@ docker compose exec archivebox archivebox manage createsuperuser # crea
mkdir -p ~/archivebox/data && cd ~/archivebox/data
docker run --rm -it -v "$PWD:/data" archivebox/archivebox:dev init
docker run --rm -it -v "$PWD:/data" archivebox/archivebox:dev install
docker run --rm -it -v "$PWD:/data" archivebox/archivebox:dev manage createsuperuser
docker run -d --name archivebox -v "$PWD:/data" -p 8000:8000 archivebox/archivebox:dev
# open http://admin.archivebox.localhost:8000 to finish setup
# docker run -it -v $PWD:/data archivebox/archivebox:dev add 'https://example.com'
# docker run -it -v $PWD:/data archivebox/archivebox:dev help
<br/>
@ -186,11 +186,10 @@ ArchiveBox is free for everyone to self-host, but we also provide support, secur
curl -fsSL 'https://docker-compose.archivebox.io' > docker-compose.yml
docker compose pull
</code></pre></li>
<li>Start the server, which initializes a new collection automatically, then create the first admin user.
<li>Start the server, which initializes a new collection automatically.
<pre lang="bash"><code style="white-space: pre-line">docker compose up -d --wait
docker compose exec archivebox archivebox manage createsuperuser
</code></pre></li>
<li>Next steps: Log in to the Admin UI at <a href="http://admin.archivebox.localhost:8000">http://admin.archivebox.localhost:8000</a>.
<li>Open <a href="http://admin.archivebox.localhost:8000">http://admin.archivebox.localhost:8000</a> and follow the setup wizard to create the first admin and configure web access.
<pre lang="bash"><code style="white-space: pre-line">
# run CLI commands inside the server container started above
docker compose exec archivebox archivebox add 'https://example.com'
@ -271,9 +270,8 @@ archivebox init # initialize a new collection
archivebox install # install all the runtime dependencies (e.g. chrome, single-file, yt-dlp, etc.)
</code></pre>
</li>
<li>Create an admin account, then optionally start the server and log in to the Admin UI at <a href="http://admin.archivebox.localhost:8000">http://admin.archivebox.localhost:8000</a>.
<pre lang="bash"><code style="white-space: pre-line">archivebox manage createsuperuser
archivebox server 0.0.0.0:8000
<li>Optionally start the server, then open the Admin UI at <a href="http://admin.archivebox.localhost:8000">http://admin.archivebox.localhost:8000</a> and follow the same setup wizard used by Docker installs.
<pre lang="bash"><code style="white-space: pre-line">archivebox server 0.0.0.0:8000
# completely optional, CLI can always be used without running a server
# archivebox [subcommand] [--help]
archivebox help
@ -308,9 +306,8 @@ archivebox add 'https://example.com'
</code></pre>
<br/>
</li>
<li>Create an admin account, then optionally start the server and log in to the Admin UI at <a href="http://admin.archivebox.localhost:8000">http://admin.archivebox.localhost:8000</a>.
<pre lang="bash"><code style="white-space: pre-line">archivebox manage createsuperuser
archivebox server 0.0.0.0:8000
<li>Optionally start the server, then open the Admin UI at <a href="http://admin.archivebox.localhost:8000">http://admin.archivebox.localhost:8000</a> and follow the same setup wizard used by Docker installs.
<pre lang="bash"><code style="white-space: pre-line">archivebox server 0.0.0.0:8000
# completely optional, CLI can always be used without running a server
# archivebox [subcommand] [--help]
archivebox help
@ -342,9 +339,8 @@ archivebox init
archivebox install
</code></pre>
</li>
<li>Create an admin account, then optionally start the server and log in to the Admin UI at <a href="http://admin.archivebox.localhost:8000">http://admin.archivebox.localhost:8000</a>.
<pre lang="bash"><code style="white-space: pre-line">archivebox manage createsuperuser
archivebox server 0.0.0.0:8000
<li>Optionally start the server, then open the Admin UI at <a href="http://admin.archivebox.localhost:8000">http://admin.archivebox.localhost:8000</a> and follow the same setup wizard used by Docker installs.
<pre lang="bash"><code style="white-space: pre-line">archivebox server 0.0.0.0:8000
# completely optional, CLI can always be used without running a server
# archivebox [subcommand] [--help]
archivebox help

View File

@ -12,11 +12,13 @@ from admin_data_views.admin import (
get_app_list as adv_get_app_list,
)
from django.contrib import admin
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth import REDIRECT_FIELD_NAME, get_user_model, login as auth_login
from django.contrib.auth.decorators import login_not_required
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.views import LoginView
from django.db import DatabaseError, connection
from django.db import DatabaseError, connection, transaction
from django.http import HttpResponseRedirect
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _
@ -103,12 +105,15 @@ class ArchiveBoxAdmin(admin.AdminSite):
from django.contrib.admin.forms import AdminAuthenticationForm
User = get_user_model()
first_admin_setup = not User.objects.filter(is_superuser=True).exclude(username="system").exists()
context = {
**self.each_context(request),
"title": _("Log in"),
"title": _("Set up ArchiveBox") if first_admin_setup else _("Log in"),
"subtitle": None,
"app_path": request.get_full_path(),
"username": request.user.get_username(),
"first_admin_setup": first_admin_setup,
}
if REDIRECT_FIELD_NAME not in request.GET and REDIRECT_FIELD_NAME not in request.POST:
context[REDIRECT_FIELD_NAME] = reverse("admin:index", current_app=self.name)
@ -116,6 +121,30 @@ class ArchiveBoxAdmin(admin.AdminSite):
index_path = reverse("admin:index", current_app=self.name)
request.current_app = self.name
if first_admin_setup:
form = UserCreationForm(request.POST or None)
if request.method == "POST" and form.is_valid():
with transaction.atomic():
# Fresh collections contain the system superuser, which also
# provides a row lock while the first real admin is created.
list(User.objects.select_for_update().filter(is_superuser=True).values_list("pk", flat=True))
if User.objects.filter(is_superuser=True).exclude(username="system").exists():
form.add_error(None, _("An admin account was already created. Log in instead."))
else:
user = form.save(commit=False)
user.is_staff = True
user.is_superuser = True
user.save()
auth_login(request, user, backend="django.contrib.auth.backends.ModelBackend")
return HttpResponseRedirect(index_path)
context["form"] = form
return TemplateResponse(
request,
self.login_template or "admin/login.html",
context,
)
return ArchiveBoxLoginView.as_view(
extra_context=context,
authentication_form=self.login_form or AdminAuthenticationForm,

View File

@ -438,16 +438,6 @@ def url_replace(context, **kwargs):
return dict_.urlencode()
@register.simple_tag
def has_real_admin_users() -> bool:
"""True if any non-``system`` superuser exists. Used by the login page to
only show the bootstrap hint (createsuperuser / ADMIN_USERNAME env vars)
when the collection still has no real admin."""
from django.contrib.auth.models import User
return User.objects.filter(is_superuser=True).exclude(username="system").exists()
@register.simple_tag(takes_context=True)
def admin_base_url(context) -> str:
return get_admin_base_url(request=context.get("request"), config=context.get("CONFIG"))

View File

@ -1,5 +1,5 @@
{% extends "admin/base_site.html" %}
{% load i18n static core_tags %}
{% load i18n static %}
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static "admin/css/login.css" %}">
{{ form.media }}
@ -18,7 +18,11 @@
{% block content_title %}
<center>
{% if first_admin_setup %}
Create your first admin account to continue setup.
{% else %}
Log in to add, edit, and remove links from your archive.
{% endif %}
</center><br/><br/>
<img src="{% static 'archive.png' %}" style="width: 80px; display: block; margin: auto"/><br/>
{% endblock %}
@ -42,6 +46,28 @@
<div id="content-main">
{% if first_admin_setup %}
<form action="{{ app_path }}" method="post" id="first-admin-form">{% csrf_token %}
<input type="hidden" name="create_first_admin" value="1">
<div class="form-row" style="gap: 0;">
{{ form.username.errors }}
{{ form.username.label_tag }} {{ form.username }}
</div>
<div class="form-row" style="gap: 0;">
{{ form.password1.errors }}
{{ form.password1.label_tag }} {{ form.password1 }}
{% if form.password1.help_text %}<div class="help">{{ form.password1.help_text|safe }}</div>{% endif %}
</div>
<div class="form-row" style="gap: 0;">
{{ form.password2.errors }}
{{ form.password2.label_tag }} {{ form.password2 }}
{% if form.password2.help_text %}<div class="help">{{ form.password2.help_text|safe }}</div>{% endif %}
</div>
<div class="submit-row" style="border: none;">
<label>&nbsp;</label><input type="submit" value="{% trans 'Create admin and continue' %}">
</div>
</form>
{% else %}
{% if user.is_authenticated %}
<p class="errornote">
{% blocktrans trimmed %}
@ -66,6 +92,7 @@
<label>&nbsp;</label><input type="submit" value="{% trans 'Log in' %}">
</div>
</form>
{% endif %}
<center>
<br/><br/>
@ -85,10 +112,6 @@ archivebox manage changepassword &lt;username&gt;
<pre>
</pre>
{% has_real_admin_users as real_admins_exist %}
{% if not real_admins_exist %}
(or set env vars <code>ADMIN_USERNAME</code> + <code>ADMIN_PASSWORD</code>)
{% endif %}
<br/>
</center>

View File

@ -164,8 +164,11 @@ async function main() {
const page = await browser.newPage();
await page.goto(config.loginUrl, {waitUntil: "networkidle2", timeout: 15000});
const firstAdminForm = await page.$("#first-admin-form");
if (!firstAdminForm) throw new Error("First admin setup form was not shown");
await page.type('input[name="username"]', config.username);
await page.type('input[name="password"]', config.password);
await page.type('input[name="password1"]', config.password);
await page.type('input[name="password2"]', config.password);
await Promise.all([
page.waitForNavigation({waitUntil: "networkidle2", timeout: 15000}),
page.click('button[type="submit"], input[type="submit"]'),
@ -202,6 +205,10 @@ async function main() {
page.waitForNavigation({waitUntil: "networkidle2", timeout: 15000}),
page.$eval('button[name="_continue"][form="machine_form"]', (button) => button.click()),
]);
await page.reload({waitUntil: "networkidle2", timeout: 15000});
if (await page.$("#archivebox-setup-wizard")) {
throw new Error("Setup wizard remained visible after BASE_URL was saved");
}
console.log(JSON.stringify({finalUrl: page.url(), bodyText: await page.$eval("body", el => el.innerText.slice(0, 500))}));
await browser.close();
@ -664,8 +671,6 @@ def test_unconfigured_public_host_superuser_can_reach_setup_wizard(tmp_path: Pat
env = cli_env(
port=port,
disable_extractors=True,
ADMIN_USERNAME="testadmin",
ADMIN_PASSWORD="testpassword",
ALLOWED_HOSTS="*",
BIND_ADDR=f"127.0.0.1:{port}",
)
@ -709,7 +714,7 @@ def test_unconfigured_public_host_superuser_can_reach_setup_wizard(tmp_path: Pat
"hostname": public_hostname,
"loginUrl": f"http://{public_host}/admin/login/?next=/admin/",
"username": "testadmin",
"password": "testpassword",
"password": "ArchiveBox-test-9vK!",
},
),
capture_output=True,

View File

@ -63,13 +63,14 @@ mkdir -p ~/archivebox/data && cd ~/archivebox
curl -fsSL 'https://docker-compose.archivebox.io' > docker-compose.yml
# (shortcut for getting https://raw.githubusercontent.com/ArchiveBox/ArchiveBox/dev/docker-compose.yml)
# pull and start the current image, then create an admin user for the Web UI
# pull and start the current image
# (the server initializes a new collection automatically)
docker compose pull
docker compose up -d --wait
docker compose exec archivebox archivebox manage createsuperuser
```
Open <http://admin.archivebox.localhost:8000> and follow the setup wizard to create the first admin and configure web access. Existing `BASE_URL` and security settings are used as-is, so configured servers skip the web-access wizard.
ArchiveBox installs and enables both ripgrep and [Sonic](https://github.com/valeriansaliou/sonic). Sonic is selected by default in the UI, while ripgrep remains available as the fallback. To select ripgrep explicitly:
```bash
docker compose exec archivebox archivebox config --set SEARCH_BACKEND_ENGINE=ripgrep
@ -198,10 +199,11 @@ docker pull archivebox/archivebox:dev
mkdir -p ~/archivebox/data && cd ~/archivebox/data
docker run --rm -it -v "$PWD:/data" archivebox/archivebox:dev init
docker run --rm -it -v "$PWD:/data" archivebox/archivebox:dev manage createsuperuser
docker run -d --name archivebox -v "$PWD:/data" -p 8000:8000 archivebox/archivebox:dev
```
Then open <http://admin.archivebox.localhost:8000> and follow the setup wizard.
*(You can create a collection in any directory you want, `~/archivebox/data` is just used as an example here)*
If you encounter permissions issues, make sure the mounted data directory is writable by its intended owner. Docker startup automatically uses the first non-root owner detected from the existing collection, or the default `archivebox` user when the data directory is root-owned.

View File

@ -76,7 +76,7 @@ If you're on macOS or Ubuntu, there is an optional auto-setup script provided.
curl -fsSL 'https://get.archivebox.io' | bash
# shortcut to run https://raw.githubusercontent.com/ArchiveBox/ArchiveBox/dev/bin/setup.sh
```
The script uses Docker Compose when available, otherwise plain Docker when it can pull the released image. If Docker is unavailable, it shows the native `uv` install plan and pauses so you can cancel before continuing. It initializes the collection, installs ArchiveBox's runtime dependencies, and starts the server; create the first admin afterward with the command printed at the end.
The script uses Docker Compose when available, otherwise plain Docker when it can pull the released image. If Docker is unavailable, it shows the native `uv` install plan and pauses so you can cancel before continuing. It initializes the collection, installs ArchiveBox's runtime dependencies, and starts the server. Open the printed Admin UI URL and follow the setup wizard to create the first admin and configure web access.
Run it as your normal user unless you want a system-owned deployment. When run as root, the script creates the `archivebox` service user and places the collection under that account's home directory; it prints the exact path and follow-up commands when finished.