# Running computer This guide takes you from cloning the repository to signing in on your own computer. Docker runs the web app, Go API, and PostgreSQL database together. You do not need to install Go, Node.js, PostgreSQL, Make, or OpenSSL separately. ## Before you start - **Docker with Docker Compose v2 or newer.** Docker Desktop includes Compose on Windows and macOS. On Linux, install Docker Engine and the Compose plugin. - **Git** to clone the repository. Alternatively, download and extract the repository ZIP from GitHub. - An internet connection for the first build to download images and packages. - Local ports **3000** and **8080** available for the app. Start Docker Desktop, or make sure the Docker service is running on Linux. Open a terminal (PowerShell on Windows) and check: ```sh docker info docker compose version ``` Both commands should succeed. On Windows, Docker must be using Linux containers. ## 1. Download the app ```sh git clone https://github.com/andrewthecodertx/computer.git cd computer ``` If you downloaded a ZIP, open a terminal in the extracted folder instead. Run the remaining commands from the folder containing `docker-compose.yml`. ## 2. Create your configuration Copy the supplied example to a file named `.env` in the repository root. Do this once on a new installation; keep an existing `.env` when updating. **macOS / Linux:** ```sh cp .env.example .env ``` **Windows PowerShell:** ```powershell Copy-Item .env.example .env ``` Generate an authentication secret using Docker. This command works in either terminal: ```sh docker run --rm node:22-alpine node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))" ``` The final line of output is a 64-character random value. Open `.env` in a text editor and replace `replace-with-a-random-secret` on the `AUTH_SECRET` line with that value. Save the file. For a new local installation, keep these other settings: ```dotenv NEXTAUTH_URL=http://localhost:3000 COOKIE_SECURE=0 ADMIN_EMAILS= POSTGRES_VOLUME=computer_pgdata POSTGRES_VOLUME_EXTERNAL=false ``` The supplied `POSTGRES_PASSWORD=computer` works with the bundled database. If you choose a different password, set it before the first startup; use a random hexadecimal value to avoid special characters in the connection URL. Keep the `OIDC_*` placeholders as supplied: use email/password login for now. Keep `.env` for future starts and upgrades. It is ignored by Git. **Keep `AUTH_SECRET` unchanged:** changing it invalidates sessions, and — unless a separate `ENCRYPTION_KEY` is set — makes saved IMAP passwords unreadable. Setting `ENCRYPTION_KEY` (a random hex string, backed up with the database) decouples integration-credential encryption from session signing: you can then rotate `AUTH_SECRET` without losing stored mailbox passwords, and rows encrypted under the old derivation are re-encrypted transparently on first use. Back up `.env` with your database backups: without the secrets it holds, encrypted integration data in a database backup cannot be decrypted. Mailbox connections always use verified TLS; plaintext IMAP logins are rejected. Use the normal IMAP ports: **993** (the default) or **143**. ## 3. Build and start ```sh docker compose up -d --build ``` The first build can take several minutes. Docker automatically: 1. Starts PostgreSQL and initializes a new database. 2. Applies any pending additive database migrations. 3. Starts the Go API. 4. Starts the Next.js web app. Check startup status: ```sh docker compose ps -a ``` Expected results: | Service | Expected state | |---|---| | `db` | Running, healthy | | `migrate` | Exited with code **0** — normal; it runs once per startup | | `app` | Running, healthy | | `web` | Running, healthy | If `db`, `app`, or `web` still says `health: starting`, wait a few seconds and check again. The `tests` service is optional and does not start during normal use. ## 4. Open the app and create your account Open **http://localhost:3000** in your browser. 1. Choose **Sign up** and create an account with your email and a password. 2. Sign in using those credentials. 3. Add a bookmark to check that saving works. With the default empty `ADMIN_EMAILS`, the first account whose email does not end in `@example.com` becomes the administrator. Create your own account first. Later accounts are regular users; an administrator can manage their roles from the Admin screen. Addresses listed in `ADMIN_EMAILS` are reserved and cannot be claimed through public signup. To create an account for one of those addresses, use the operator-provisioning command in [README.md](README.md#developer-commands). The default setup is accessible only from this computer. Public bookmark links are also local until you configure externally accessible hosting. ### What to test - Create, edit, and delete a bookmark, including its URL, description, and tags. - Organize bookmarks using tags, calendar dates, contacts, and kanban columns. - Create a collection, add an existing bookmark to it (nothing is duplicated), add a select property such as Status, and switch between the table and a board view over the same membership. - Write a Markdown page, switch between pages, and download its Markdown export. - Share a bookmark, tag, or date with a second account and revoke the share. - Open a public bookmark link in a signed-out browser window. Pages autosave after a short pause and finish saving before switching page tabs. If saving fails, the page remains **Unsaved** and a draft is kept in the current browser tab. Reconnect and click **Save now**; tab-local drafts are not a replacement for database backups. If another tab or device saved the same page first, autosave pauses with a conflict banner — choose explicitly between overwriting, saving a copy, or discarding; typing alone never overwrites the newer version. ## Everyday commands Run these from the repository root: | Task | Command | |---|---| | Stop the app and keep your data | `docker compose down` | | Start it again | `docker compose up -d` | | Restart the web app and API | `docker compose restart app web` | | Check service status | `docker compose ps -a` | | Follow application logs | `docker compose logs -f app web` | Press **Ctrl+C** to stop following logs; the app keeps running. After changing `.env`, use `docker compose up -d` to recreate affected containers with the new settings. A simple `restart` does not load changed environment variables. ### Where your data lives Bookmarks, accounts, pages, and settings live in the Docker PostgreSQL volume named `computer_pgdata` by default. They survive normal stops, container recreation, and image rebuilds. **Do not use `docker compose down -v` or delete that volume if you want to keep your data.** The volume is not a backup; back up the database separately. ### Backups and restore Take dumps with the database running (review OPS-03: a dump is only a backup once a restore from it has been proven): ```sh docker compose exec -T db pg_dump -U computer -d computer | gzip > computer-$(date -u +%Y%m%d).sql.gz cp .env env-$(date -u +%Y%m%d).bak # AUTH_SECRET / ENCRYPTION_KEY / POSTGRES_PASSWORD ``` Keep the dump **and** the `.env` snapshot together and copy both off-host: stored IMAP passwords are AES-GCM ciphertext, and without the secret they were encrypted under, a restored database cannot decrypt them. The deployment entry point (`scripts/deploy.sh`, which runs `scripts/server-deploy.sh` on the server) does this automatically before every deploy into `/var/backups/computer` (0600 dumps, 15 retained); it starts a stopped database rather than skipping the backup silently. Restore into an isolated environment first to verify a dump before trusting it. Use a separate checkout (or stop the live stack first) and a scratch volume so the live `computer_pgdata` volume is never touched: ```sh POSTGRES_VOLUME=computer_restore docker compose up -d db gunzip -c computer-YYYYMMDD.sql.gz | docker compose exec -T db psql -U computer -d computer # inspect the restored data, then remove the scratch container and volume docker compose rm -sf db docker volume rm computer_restore ``` Then restore the matching `.env` so the secrets can decrypt integration data, and start the stack against the real volume. Migrations are additive, so any current or newer application release reads an older dump; reverting to an older data state means restoring that dump, not reverting SQL. ### Updating the app Back up your database and `.env` before updating. Then run: ```sh git pull --ff-only docker compose up -d --build docker compose ps -a ``` Keep your existing `.env` and database volume. Database migrations run automatically during startup. The bundled PostgreSQL image already includes the `pg_trgm` extension used by page search; the database owner role must be allowed to create extensions if you point the app at an external database. ## Optional integrations The core app works without any external services. - **IMAP email:** In **Settings → IMAP**, enter your mail server, port (**993** or **143**), TLS setting, username, password, and folder. Test the connection, then save. Add an email watcher from a bookmark's Signals panel. Enabled watchers are checked every five minutes while the API is running. - **Nextcloud contacts:** In **Settings → Nextcloud**, enter your Nextcloud URL, username, and app password, then sync contacts. You can associate a synced contact with a bookmark. - **Browser alerts:** Allow notifications when prompted. Alert checks run while the app is open in your browser. - **Authelia:** This integration is intentionally unfinished. Setting OIDC environment variables alone does not enable sign-in. See [web/docs/AUTHELIA.md](web/docs/AUTHELIA.md) for integration details. ## Troubleshooting ### Docker cannot connect to its daemon Start Docker Desktop or the Linux Docker service, then retry `docker info`. On Linux, ensure your account has permission to use Docker. ### `docker compose` is not recognized Install the Docker Compose plugin or update Docker Desktop. These instructions use `docker compose` with a space, rather than the older `docker-compose`. ### `Set AUTH_SECRET in .env` Confirm `.env` exists beside `docker-compose.yml`, has a nonempty `AUTH_SECRET`, and was not saved as `.env.txt`. Follow step 2 to generate the value. ### Port 3000 or 8080 is already in use Stop the other application or Docker stack using that port, then retry `docker compose up -d --build`. Alternatively move this stack's host ports in `.env` (`WEB_HOST_PORT`, `APP_HOST_PORT`). When you shift `WEB_HOST_PORT`, `NEXTAUTH_URL` must be updated to the same browser-facing origin (for example `http://localhost:3100`): the API proxy rejects requests whose Origin matches neither, so sign-in works but saving fails with "Invalid request origin". Recreate the stack with `docker compose up -d` after changing `.env`. ### The app does not open, or sign-in fails Use **http://localhost:3000** consistently; keep `NEXTAUTH_URL` set to that address and `COOKIE_SECURE=0` for local HTTP. Then check: ```sh docker compose ps -a docker compose logs --tail=100 db migrate app web ``` The API's database readiness check is available in your browser at **http://localhost:8080/readyz**. A ready API returns `{"status":"ready"}`. If PostgreSQL reports a password mismatch on an existing volume, restore the original `POSTGRES_PASSWORD` in `.env`. Changing that variable does not change the password of a database that was already initialized. When asking for help, include service status and relevant error messages. Remove passwords, secrets, and personal data from anything you share. ## Running on a remote server The default Compose ports bind to `127.0.0.1`. For remote access, configure an HTTPS reverse proxy to the web app on port 3000, set `NEXTAUTH_URL` to your public HTTPS URL, and set `COOKIE_SECURE=1`. The database remains internal to Docker. ### Rate-limit identity behind a proxy Set `TRUSTED_PROXY=1` in `.env` when (and only when) the proxy in front of the web container **overwrites** `X-Real-IP` with the browser's address — the bundled nginx template does, and `scripts/server-deploy.sh` writes the flag for server deployments. The Next server then signs that address and forwards it to Go, so each browser gets its own login/signup/public-endpoint bucket. Without it every client shares the web container's bucket (20 logins/minute for the whole instance). Do **not** set `TRUSTED_PROXY=1` while port 3000 is reachable directly: clients could then choose their own rate-limit identity. The web process logs a startup warning when `NEXTAUTH_URL` is HTTPS but the flag is missing. ### First-run administrator on an exposed server The default empty `ADMIN_EMAILS` plus bootstrap lets the first signup become administrator. That is fine locally, but on a public host it is a race with whoever registers first after DNS goes live. For exposed deployments: 1. Put `ALLOW_BOOTSTRAP_SIGNUP=0` in `.env` (server-deploy.sh writes it). 2. List the operator in `ADMIN_EMAILS=you@example.com` and restart the app. 3. Provision the account with `SEED_EMAIL=you@example.com SEED_PASSWORD='...' make seed`. `SIGNUP_ENABLED=0` additionally closes self-service registration. Public signup can never grant `ADMIN` after bootstrap is disabled; recovery is operator-driven. ### Large-install index maintenance Migration `007` builds GIN trigram indexes on `Page(title)` and `Page(content)`. The migration runner wraps each file in one transaction, so `CREATE INDEX CONCURRENTLY` is unavailable and the build holds locks that block writes for its duration. Fresh and small installations are fine; on a large existing `Page` table, schedule a maintenance window, or pre-build the indexes yourself before deploying (the migration uses `IF NOT EXISTS` and becomes a no-op): ```sql CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE INDEX CONCURRENTLY IF NOT EXISTS "Page_title_trgm_idx" ON "Page" USING gin (title gin_trgm_ops); CREATE INDEX CONCURRENTLY IF NOT EXISTS "Page_content_trgm_idx" ON "Page" USING gin (content gin_trgm_ops); ``` Run those through `make db-shell` (they cannot run inside a transaction). ### Bundled deployment script The repository ships a target-agnostic deployment for exactly this setup — no host or domain is hardcoded. Prerequisites: a Debian/Ubuntu server with ssh access (an ssh config alias works well), DNS for your domain already pointing at it, and `rsync` + `git` on your workstation. Then, from the repository root: ```sh HOST=app-server DOMAIN=computer.example.tld CERTBOT_EMAIL=ops@example.tld ./scripts/deploy.sh ``` The script deploys a **known git revision** (dirty working trees are refused unless `ALLOW_DIRTY=1`) and on the server provisions Docker/nginx/certbot, writes `.env` on first deploy, takes a pre-deploy database dump plus `.env` snapshot into `/var/backups/computer` (0600, 15 retained), builds revision-tagged images, renders the nginx + Let's Encrypt proxy from `deploy/nginx/computer.conf.template`, and verifies `https://DOMAIN/login` end to end. Rollback: re-deploy an older revision (`git checkout && ./scripts/deploy.sh`), or restore one of the pre-deploy dumps together with its `.env` snapshot. Database migrations are additive, so an older application release keeps working against a migrated database; rolling the data itself back means the dump, not reverting SQL.