[
  {
    "path": ".gitignore",
    "content": "secrets/\n\n# Maintenance log. Various scripts write to.\ntalkyard-maint.log\n\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"versions\"]\n\tpath = versions\n\turl = https://github.com/debiki/talkyard-versions.git\n"
  },
  {
    "path": "LICENSE-MIT.txt",
    "content": "The things in this repository are licensed under the MIT license, see below. The actual Talkyard source code is in another repository, under a different license.\n\n---\n\nCopyright (c) 2016-2024 Kaj Magnus Lindberg\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "Installing Talkyard\n================\n\nNOTICE: This Git branch is a PREVIEW of the upcoming Talkyard v1 (epoch 1).\n\nYou need to edit `.env` and set `RELEASE_BRANCH=tyse-v1-dev` (even though the\ncomment juts above in `.env` says \"don't use\").\n\nDo not use this preview of v1 \"for real\", in production, yet.\n\nAlso, don't install v0 — because then you'd want to upgrade to v1 pretty soon,\na waste of time.\n\nInstead: **Wait 1 or 2 weeks until v1 has been released** — that should be in the beginning\nof April 2026, or end of Mars. Then install v1.\n\nFeedback welcome! You can post in: https://forum.talkyard.io\n\n------\n\nHere you'll learn how to install Talkyard v1 on a single server, for production use:\nDebian 12 or 13 with at least 2 GB RAM.\n\n<small>(Old Talkyard v0 docs are <a href=\"https://github.com/debiki/talkyard-prod-one/tree/ty-prod-one-v0\">\n  here</a>.)</small>\n\n<!-- Leave a comment here for example: https://forum.talkyard.io/-857/release-talkyard-v1 -->\n\nDocker-based installation.\nAutomated upgrades and backups.\nAutomatic HTTPS certs.\nMulti-site support.\n\n<!-- NO, Swarm is abandonware\nIf however you already have a Docker-Compose or Docker Swarm installation\nwith a HTTPS reverse proxy, and want to add Talkyard to it,\nthen have a look at: https://github.com/debiki/talkyard-prod-swarm.\n-->\n\n\nYou should be familiar with Linux, Bash, Git and Docker.\nOr use our hosting, see https://www.talkyard.io.\n<!-- Otherwise you might run into\nproblems. For example, there might be Git edit conflicts, if you and we change\nthe same file — then you need to know how to resolve those edit conflicts.\nAlternatively, there's paid hosting, see: https://www.talkyard.io/pricing/.\n-->\n\nAsk questions and report problems in **[the forum](http://www.talkyard.io/forum/latest/support)**.\n\n<!-- This now fixed, using Docker volumes & logging instead, others cannot access.\n### Security: *Private* server\n\nDon't give people-you-don't-absolutely-trust ssh access to your Talkyard server.\nThe database files in `/opt/talkyard/data/rdb/` are accessible to people who can\nssh into the server, and log files in `/var/log/` are, too.\nThis'll change in Talkyard v1 (next year 2025?) — then we'll use Docker volumes instead.\n-->\n\n<!-- [vagrant_or_not]  Move Vagrantfile  to old/  ?\n### Install on your laptop?\n\nHere's [a Vagrantfile here](https://github.com/debiki/talkyard-prod-one/blob/main/scripts/Vagrantfile)\nif you want to test install on a laptop\n<!-- relative links:  scripts/Vagrantfile  don't work in Docusaurus, it doesn't know how\nto render a Vagrantfile. So we're linking to GitHub. Oh well. - ->\n— open the Vagrantfile in a text editor, and read, for details.\n(It's old, maybe won't work.)\n-->\n\n\n<!--\n### Install behind an Nginx reverse proxy? -->\n\n<!-- Someone tried to do this, although in his case, there was *no* reverse proxy. -->\n<!-- Move to docs/ file, and update path:  /opt/talkyard/conf/play-framework.conf  —>  .../conf/app/play-framework.conf  ? [2doc]\nTo install Talkyard behind a reverse proxy, read here: docs/reverse-proxy.md.\n(If you don't know what a reverse proxy is, just ignore this.)\n -->\n<!--\nSkip this, unless you know what a \"reverse proxy\" is;\ninstead, continue below, the section \"Install on a new server\".\nNow, if you _do_ want to install Talkyard on a Debian or Ubuntu server\nwith a Nginx reverse proxy in front of it, with a LetsEncrypt cert — then,\n[here's a mini tutorial](https://www.talkyard.io/-389/talkyard-with-nginx-as-reverse-proxy-and-letsencrypt-for-https-mini-tutorial).\nThe steps 1, 2, 3 ... in that tutorial, are the steps 1, 2, 3 ... below.\n-->\n\n\n<!--\n### Install on a new server\n\nThe rest of this document is about how to install Talkyard on a new server.\n-->\n\n**Installation overview:** You'll rent a virtual private server (VPS), download\nand install Talkyard, sign up for a send-emails service, and configure email settings.\nThen optionally configure OpenAuth login for Google, Facebook, Twitter, GitHub.\nAnd off-site backups.\n\nDockerfiles, build scripts and source code are in another repo: https://github.com/debiki/talkyard.\nSee `./docker-compose.yml` (in this repo) for details and links.\n\n\nDirectories\n----------------\n\nYou'll install Talkyard <!-- -the-software, and config files, --> in `/opt/talkyard-v1/`.\n\n<!--\n(`-v1` is for \"host scripts version one\". Every 3? 5? years, there's a major\nnew version of the host scripts, and you'll install in /opt/talkyard-vX/,\nand import a backup.) -->\n\nTalkyard uses these directories:\n(following the Linux File System Hierarchy Standard)\n<!-- FHS, Debian: https://manpages.debian.org/bookworm/manpages/hier.7.en.html\nShouldn't use /opt/backups for backups?  o.O\nThey write:  \"/var/backups  Reserved for historical reasons.\"\nAnd, https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch05s02.html: \"Several directories\nare `reserved' in the sense that they must not be used arbitrarily by some new application,\nsince they would conflict with historical and/or local practice. They are:\n/var/backups, /var/cron, ...\".  Better store backups in /var/opt/...backups.../ somewhere?\n-->\n\n- `/opt/talkyard-v1/`: Various scripts, and `docker-compose.yml`.\n      This is a Git repo — you can check in your changes to Git,\n      but only if you can resolve Git conflicts!\n      <!--\n      if you `git fetch` new minor versions of these scripts.\n      We call scripts here \"host scripts\" since they run on the host operating system.\n      They aren't part of Talkyard itself — none of them would be relevant, if\n      instead running Ty on Windows (not supported).\n      -->\n- `/opt/talkyard-v1/conf`:\n    Configuration, mounted read-only in Docker containers.\n- `/opt/talkyard-v1/secrets`:\n    Docker secrets, e.g. database password.\n- `/var/lib/docker/`:\n    Database storage, uploaded files (in Docker volumes).\n    Docker images, log files.\n- `/var/opt/backups/talkyard/v1/`:\n    Backups.\n\n\nPreparations\n----------------\n\n1.\n   Provision a Debian 12 or 13 server, <!-- not 11, it's EOL 2026 --> or Ubuntu 24.04,\n   with at least 20 GB disk and 2 GB RAM,\n   for example at <!-- [Digital Ocean](https://www.digitalocean.com/), a US company, -->\n   [Upcloud](https://upcloud.com/).\n\n   Point a domain name, say, `forum.your-website.com`, to the server IP address.\n\n1.\n   Update the OS, then install Git and some stuff:\n\n       apt-get update\n       apt-get upgrade\n       apt-get -y install git locales\n       apt-get -y install rng-tools        # better generation of random numbers\n       apt-get -y install jq               # to view logs\n       apt-get -y install tree ncdu vim    # nice to have\n       locale-gen en_US.UTF-8              # installs English\n       export LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8  # starts using English (warnings are harmless)\n\n1.\n   Create big empty files that you can delete if your server runs out of disk:\n\n       fallocate --length 250MiB /balloon-1-delete-if-disk-full\n       fallocate --length 250MiB /balloon-2-delete-if-disk-full\n       fallocate --length 250MiB /var/balloon-3-delete-if-disk-full\n       fallocate --length 250MiB /var/balloon-4-delete-if-disk-full\n\n1.\n   Install Docker.\n   Read: https://docs.docker.com/engine/install/debian/ and follow the instructions.\n   Or use their convenience script: https://docs.docker.com/engine/install/debian/#install-using-the-convenience-script.\n\n   Afterwards, also install the Docker Compose plugin:\n\n       sudo apt-get install docker-compose-plugin\n\n   (Optionally, add: `{ \"log-driver\": \"local\" }` to `/etc/docker/daemon.json`,\n   so Docker will delete old logs for all your Docker containers, and save disk.\n   But you don't need to — Talkyard uses that logging driver by default in any case.)\n\n<!-- This is in  docker-compose.yml  already, see 'x-logging: &default_logging'.\n1.\n   Configure Docker log rotation, so you won't run out of disk.\n   You can use the `local` log driver — it cleans up old log files automatically\n   (see https://docs.docker.com/engine/logging/drivers/json-file/).\n   In `/etc/docker/daemon.json`:\n\n       {\n         \"log-driver\": \"local\"\n       }\n-->\n\n\n### Advanced\n\nIf you want to, and know what you're doing:\n\n**Swap:** Comment out any swap from `/etc/fstab`, and run: `swapoff -a`.\n\n**Disks:** Mount `/var/` and `/var/opt/backups/(talkyard/)` on their own disks\n(so the host OS and Talkyard won't stop working just because some disk\ngets full).\n\n<!-- Let's not mention this. Too complicated, and almost never needed.\nIf you expect people to upload lots of big files, you could create your\nown custom 'pub-files' and 'priv-files' volumes, and mount on their own disk\n— see docker-compose.yml, the 'volumes:' section.\nOr connect to some S3 compatible cloud storage (not yet implemented `[cloud_storage]`).\n-->\n\n**Firewall:** Install a firewall, for example, firewalld, see: https://firewalld.org.\n\nNote that ufw (another Linux firewall) is incompatible with Docker\n— Docker can bypass `ufw` rules, see:\nhttps://docs.docker.com/engine/network/packet-filtering-firewalls/#docker-and-ufw.\nThere is, however, [ufw-docker](https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html#recommended-mitigations).\n<!-- [firewalld_not_ufw] update script, have it use firewalld  -->\n\nYou can see the IP addresses of the Docker containers in the `.env` file. The IP\nof the `web` container, which runs Nginx and listens on ports 80 and 443,\nis set on the `FE_PUB_NET_WEB_IP=...` line, and this is the only\ncontainer that should be reachable from outside. The egress proxy should be able\nto connect to the Internet (but no one should be able to connect to it). Its\nIP address is set on the `EG_PUB_NET_EGRESSP_IP=...` line.\nMore about Firewalld and Docker:\nhttps://firewalld.org/2024/04/strictly-filtering-docker-containers\n\nIf you use Google Cloud Engine: GCE already has a firewall.\n\n\nInstallation instructions\n----------------\n\n(There's a troubleshooting document here: ./docs/troubleshooting.md )\n\n<!-- The newline after '1.' just below is needed for Docusaurus to render the code\n    block properly? I don't totally remember. -->\n\n1.\n   Download installation scripts: (you need to install in\n   `/opt/talkyard-v1/` for the backup scripts to work)\n\n       sudo -i  # become root\n       cd /opt/\n       git clone https://github.com/debiki/talkyard-prod-one.git talkyard-v1\n       cd talkyard-v1\n       # Make sure you'll install v1:\n       git checkout --track origin/ty-prod-one-v1\n\n1.\n   Prepare the OS: install tools, enable automatic security updates, simplify troubleshooting,\n   and make ElasticSearch work: (Consider reading the script first...)\n\n       ./scripts/prepare-os.sh 2>&1 | tee -a talkyard-maint.log\n\n   If you don't want to run the whole script, you at least need to:\n\n   -  Copy the sysctl `net.core.somaxconn` and `vm.max_map_count` settings in the script to your\n       `/etc/sysctl.conf` config file — otherwise, the full-text-search-engine (ElasticSearch)\n       won't work. Afterwards, run `sysctl --system` to reload the system configuration.\n\n1. Edit config values:\n\n   ```\n   nano conf/app/play-framework.conf   # fill in values in the Required Settings section\n\n   nano secrets/postgres_password.txt  # type a database password on a single line, nothing else!\n\n   # Don't let anyone see the password.\n   chmod 0600 secrets/postgres_password.txt\n   ```\n\n   Note:\n   - Set `talkyard.secure=true`, so HTTPS will work — unless you're testing\n     on localhost; then set `talkyard.secure=false`.\n   - If you don't edit `play.http.secret.key` in `play-framework.conf`,\n     the server won't start.\n   - A PostgreSQL database user, named *talkyard*, gets created automatically,\n     by the *rdb* Docker container, with the password you specified in\n     `secrets/postgres_password.txt`.\n     You don't need to do anything.\n    <!-- Do people use Vagrant nowadays? [vagrant_or_not] In any case, shouldn't the *web*\n      container, not the *app*, listen to 8080?\n   - If you're using a non-standard port, say 8080 (which you do if you're using **Vagrant**),\n     then comment in `talkyard.port=8080` in `play-framework.conf`.\n    -->\n\n1. Depending on how much RAM your server has (run `free -mh` to find out),\n   choose one of these files:\n   mem/1.7g.yml, mem/2g.yml, mem/3.6g.yml, ... and so on,\n   and copy it to ./docker-compose.override.yml. For example, for\n   a server with 4 GB RAM:\n\n        cp mem/4g.yml docker-compose.override.yml\n\n1. Install and start the latest version. Might take a few minutes\n   the first time (to download Docker images).\n\n        # This script also installs, although named \"upgrade–...\".\n        ./scripts/upgrade-if-needed.sh 2>&1 | tee -a talkyard-maint.log\n\n   (This creates a new Docker network — you can choose the IP range; see the\n   section *Docker Networks* below.)\n\n   Type `docker compose ps` — you should now see a list\n   of Docker containers in state Up (means they're running).\n\n1. Schedule daily backups, deletion of old backups, and automatic upgrades:\n\n        ./scripts/schedule-daily-backups.sh 2>&1 | tee -a talkyard-maint.log\n        ./scripts/schedule-automatic-upgrades.sh 2>&1 | tee -a talkyard-maint.log\n\n    <!-- Script for CGE:\n\n    # m h  dom mon dow   command\n    @reboot echo '---REBOOT---' >> /opt/talkyard-cron.log\n    @reboot echo '/opt/talkyard-mount-backups-bucket.sh >> /opt/talkyard-cron.log 2>&1' | at now + 5 minutes\n    10 0 * * * cd /opt/talkyard && ./scripts/delete-old-logs.sh >> talkyard-maint.log 2>&1\n    10 2 * * * cd /opt/talkyard && ./scripts/backup.sh daily >> talkyard-maint.log 2>&1\n    10 3 * * * cd /opt/talkyard && ./scripts/delete-old-backups.sh >> talkyard-maint.log 2>&1\n    51 0 * * * cd /opt/talkyard && ./scripts/renew-https-certs.sh >> talkyard-maint.log 2>&1\n\n    root@tyc-nnnnnnnnnnn:~# cat /opt/talkyard-mount-backups-bucket.sh  \n    #!/bin/bash\n    mkdir -p /opt/talkyard-backup-archives-in-gcs\n    /usr/bin/gcsfuse cloud-storage-bucket-name /opt/talkyard-backup-archives-in-gcs\n    -->\n\n1. Open a web browser; go to `https://talkyard.your website.com` — note: **https**\n   not http.\n\n   Your browser should show a warning about the connection _not_ being secure.\n   Talkyard and LetsEncrypt will now start generating a HTTPS certificate for you.\n   Wait 20 seconds, reload the page, and thereafter HTTPS should work.\n\n   <!-- But now it's  `docker compose logs -f --tail 99 web`  instead?\n   **(** If you'd look in the Nginx log, `tail -f /var/log/nginx/error.log`,\n   you'd see messages like:\n\n   ```\n   domain_whitelist_callback(): Should have cert: talkyard.example.com\n   update_cert_handler(): order rsa cert for talkyard.example.com\n   SSL_do_handshake() failed (SSL: error:... alert bad certificate: SSL alert number 42) while SSL handshaking\n   Replying to ACME HTTP-01 challenge, server name: _, host: talkyard.example.com\n   update_cert_handler(): new rsa cert for talkyard.example.com is saved\n   ```\n   (The \"failed ... alert number 42\" is fine\n   — it's because, at that time, there wasn't yet any cert.) **)**\n   -->\n\n   <!-- [vagrant_or_not]\n   However, if you're testing on localhost, or with Vagrant,\n   instead go to <http://localhost>, or <http://localhost:8080>, respectively.\n   (And you'll need `talkyard.secure=false` in `play-framework.conf`).\n    -->\n\n1. In the browser, click _Continue_ and create an admin account\n   with the email address you specified when you edited `play-framework.conf` earlier\n   (see above).\n   Follow the getting-started guide.\n\nEverything will restart automatically on server reboot.\n\nNext steps:\n\n<!--\n- Do not enable HTTP2, currently doesn't work with Nginx + the Lua module (apparently [this](https://github.com/openresty/lua-nginx-module/blob/52af63a5b949d6da2289e2de3fb839e2aba4cbfd/src/ngx_http_lua_headers.c#L116) error happens).\n  Update 2021-03: Works fine w OpenResty, if avoiding  ngx.location.capture [63DRN3M75]\n-->\n- Edit `/opt/talkyard-v1/conf/web/sites-enabled/talkyard-servers.conf` and redirect\n  from HTTP to HTTPS.<br/>\n  <!-- This is very rare and a bit advanced. Also, there's not just Certbot\n  nowadays, but also e.g. Lego https://github.com/go-acme/lego which might\n  be a better choice. So skip this:\n  (If you for some reason want to run LetsEncrypt's Certbot yourself to generate\n  a HTTPS cert, see [docs/setup-https.md](docs/setup-https),\n  and have a look at the commented out `server {}` block at the bottom of\n  `talkyard-servers.conf`.)\n  -->\n- Sign up for a send-email-service — see the section just below.\n- Send an email to `hello at talkyard.io` so we get your address, and can\n  inform you about security issues and major software\n  upgrades that might require you to do something manually.\n  Or subscribe to the Announcements category over at https://www.talkyard.io/forum/.\n- Copy backups off-site, regularly. See the Backups section below.\n- Configure Gmail, Facebook, Twitter, GitHub login,\n    by creating OpenAuth apps over at their sites, and adding API keys and secrets\n    to `play-framework.conf`. See below, just after the next section, about email.\n- Optionally, create more Talkyard sites hosted by this same Talkyard installation,\n  see [docs/multisite-talkyard.adoc](docs/multisite-talkyard.adoc).\n\n\nConfiguring email\n----------------\n\nIf you don't have a mail server already, then sign up for a transactional email\nservice, for example Mailgun, Elastic Email, SendGrid, Mailjet or Amazon SES.\n(Signing up, and verifying your sender email address and domain, is a bit complicated\n— nothing you do in five minutes.)\n\nThen, configure email settings in `/opt/talkyard/conf/play-framework.conf`,\nthat is, fill in these values:\n\n```\ntalkyard.smtp.host=\"...\"\ntalkyard.smtp.port=\"587\"\ntalkyard.smtp.requireStartTls=true\n#talkyard.smtp.tlsPort=\"465\"\n#talkyard.smtp.connectWithTls=true\ntalkyard.smtp.checkServerIdentity=true\ntalkyard.smtp.user=\"...\"\ntalkyard.smtp.password=\"...\"\ntalkyard.smtp.fromAddress=\"support@your-organization.com\"\n```\n\n(Google Cloud Engine blocks outgoing ports 587 and 465 (at least it did in the past).\nProbably you email provider has made other ports available for you to use,\ne.g. Amazon SES: ports 2587 and 2465.)\n\n\nOpenAuth login\n----------------\n\nYou want login with Facebook, Gmail and maybe Twitter and GitHub to work? Here's how.\n\nHowever, we haven't written easy to follow instructions for this yet.\nSend us an email: `hello at talkyard.io`, mention OpenAuth, and we'll hurry up.\n\n<small>(There are very very brief instructions in this the markdown source but they might be out of date,\nor there might be typos,\nso they're hidden unless you are a tech person who knows how to view the source.)</small>\n\n<!-- The \"hidden\" instructons.\nYou can try to follow the instructions below, and maybe won't be easy.\n\nThe login callbacks that you will need to fill in, are\n`http(s)://your.website.com/-/login-auth-callback/NAME` where *NAME* is\none of `google`, `twitter`, `facebook`, `github`.\n\nThe \"copy-paste\" instructions below are for `/opt/talkyard/conf/play-framework.conf`,\nat the end of the file.\n\nFacebook:\n\n - Go to https://developers.facebook.com, and sign up or log in\n - Select the **My Apps** menu to the upper right\n - Click **Add New App**\n - Create a *Products | Facebook Login* app. (We should write more about this and\n   add screenshots.)\n - Copy-paste the Facebook app id into `#facebook.clientID=\"...\"` and `#facebook.clientSecret=\"...\"`\n   (instead of the `...`), and activate (\"comment in\") each line by removing the `#`.\n\nGmail:\n\nFirst, consider visiting https://developers.google.com/people/v1/getting-started#1.-get-a-google-account\n  and reading the instructions.\n\nThen let's get started for real:\n- Go to Google's People API setup tool: https://console.developers.google.com/start/api?id=people.googleapis.com&credential=client_key\n- Select an existing project of yours, or create a new one.\n- Click Continue.\n- You should see a message \"People API has been enabled\" in the upper left corner.\n- Click \"Go to credentials\"\n- You should see: \"Find out what kind of credentials you need\".\n  (If you get lost, you can go back to here, by clicking the upper left corner\n  hamburger menu, then choosing \"APIs & Services\", then clicking \"Credentials\",\n  then in the \"Create credentials\" dropdown, selecting \"Help me choose\". )\n\n- In the \"Which API are you using?\" dropdown, select \"People API\".\n- In the \"Where will you be calling the API from?\" dropdown, select \"Web server\".\n- Below \"What data will you be accessing?\", select \"User data\".\n- Click \"What credentials do I need\", and proceed with creating credentials if needed.\n\n- Now you need to fill in fields for an OAuth Consent dialog. This dialog is where\n  your users see your organization's name, URL and logo, and can read about\n  how you handle their data — you need to add a link to a Privacy Policy,\n  and Terms of Use. If you don't have your own Privacy Policy and ToU, then,\n  you can use these:\n    https://YOUR_TALKYARD_SERVER/-/privacy-policy\n    https://YOUR_TALKYARD_SERVER/-/terms-of-use\n\n- You'll get to a page \"Client ID for Web application\".\n  There, in the \"Authorized redirect URIs\" field, type:\n    https://YOUR_TALKYARD_SERVER/-/login-auth-callback/google\n\n    (Ignore the \"Authorized JavaScript origins\" field.)\n\n- (Old? blog post w photos:\n    https://medium.com/@pablo127/google-api-authentication-with-oauth-2-on-the-example-of-gmail-a103c897fd98 )\n\nTwitter:\n - Go to https://apps.twitter.com, sign up or log in.\n - Click **Create New App**\n - As callback URL, specify: `https://your.website.com/-/login-auth-callback/twitter`\n - Copy-paste your key and secret into `#twitter.consumerKey=\"...\"` and `#twitter.consumerSecret=\"...\"`,\n   and remove the `#`.\n\nGitHub:\n - Log in to GitHub. Click your avatar menu. Then Settings, then Developer Settings, OAuth Apps.\n - Copy-paste your client ID and secret into `#github.clientID=\"...\"` and `#github.clientSecret=\"...\"`,\n   and remove the `#`.\n-->\n\n\nViewing log files\n----------------\n\nChange directory to `/opt/talkyard-v1/`. Then:\n\n- The application server, to view its logs: `./view-logs -f --tail 50 app`\n  &thinsp; (where `-f --tail NN` is optional).\n  You can also: `docker compose logs -f --tail 50 app`, but then you'll see\n  hard to read json. `view-logs` uses `jq` to parse & make readable the json.\n- The web server:  `docker compose logs -f --tail 50 web` (not json).\n- The database:  `docker compose logs -f --tail 50 rdb` (not json).\n- The search engine: `./view-logs search`.\n\n\nUpgrading to newer versions\n----------------\n\nIf you followed the instructions above — that is, if you ran these scripts:\n`./scripts/prepare-os.sh` and `./scripts/schedule-automatic-upgrades.sh`\n— then your server should keep itself up-to-date, and ought to require no maintenance.\n\nIn a few cases you might have to do something manually, when upgrading.\nLike, running `git pull` and editing config files, maybe running a shell script.\nFor us to be able to tell you about this, please send us an email at\n`hello at talkyard.io`.\n\nIf you didn't run `./scripts/schedule-automatic-upgrades.sh`, you can upgrade\nmanually like so:\n\n    sudo -i\n    cd /opt/talkyard-v1/\n    ./scripts/upgrade-if-needed.sh 2>&1 | tee -a talkyard-maint.log\n\n\n\nBackups\n----------------\n\n### Importing a backup\n\nSee [docs/how-restore-backups.md](./docs/how-restore-backup.md).\n\n\nYou can log in to Postgres like so:\n\n    sudo docker compose exec rdb psql postgres postgres  # as user 'postgres'\n    sudo docker compose exec rdb psql talkyard talkyard  # as user 'talkyard'\n\n\n### Backing up, manually\n\nYou should have configured automatic backups already, see the Installation\nInstructions section above. In any case, you can backup manually like so:\n\n    sudo -i\n    cd /opt/talkyard-v1/\n    ./scripts/backup.sh manual 2>&1 | tee -a talkyard-maint.log\n\nNew backups should appear in `/var/opt/backups/talkyard/v1/archives/`.\n\n\n### Copy backups elsewhere\n\nYou should copy the backups to a safety off-site backup server, regularly.\nOtherwise, if your main server suddenly disappears, or someone breaks into it\nand ransomware-encrypts everything — you'd lose all data.\n\nSee [docs/copy-backups-elsewhere.md](./docs/copy-backups-elsewhere.md).\n\n<!--\nThere's also a script you can copy-paste to that off-site backup server,\nand run daily via Cron, to get notified via email if backups stop working\n— but no, not yet implmented `[BADBKPEML]`.\n-->\n\n\nDocker networks\n----------------\n\nTalkyard creates its own Docker subnets, for security reasons, and assigns static\nIPs to the containers.\nWithout static IPs, then, if a container restarts, Docker might give it a new IP,\nand the other containers then couldn't find it it. —\nUnless they're also restarted, so all things that have cached the old stale IP,\npick up the new IP instead.\n\nYou can choose the network IP ranges. Open the `.env` file, scroll down and you'll see:\n\n```\nFE_PUB_NET_SUBNET=...\nFE_INT_NET_SUBNET=...\nBE_INT_NET_SUBNET=...\n...\n```\n\n\n\nTips\n----------------\n\nIf you start running out of disk, one reason can be old patches for automatic operating system security updates.\nYou can delete them to free up disk:\n\n```\nsudo apt autoremove --purge\n```\n\n\n\n\nLicense (MIT)\n----------------\n\n```\nCopyright (c) 2016-2025 Kaj Magnus Lindberg.\n\nLicensed under the MIT license, see `LICENSE-MIT.txt` — and this is for the\ninstructions and scripts in this repository only, not for Talkyard source code\nor things in other repositories.\n```\n\n\n<!-- vim: set et ts=2 sw=2 tw=0 fo=r list : -->\n"
  },
  {
    "path": "conf/app/play-framework.conf",
    "content": "# Play Framework configuration file\n\n\n# Required settings\n# ---------------------\n\n# Fill in your email address. Later on, sign up with this email, to become the site owner.\ntalkyard.becomeOwnerEmailAddress=\"your-email@your.website.org\"\n\n# The address to your Talkyard site, e.g. \"forum.your-organization.org\" or\n# \"comments.your.blog\".\n# This address is used when Talkyard generates links back to itself, e.g. links in emails.\n# And so your Talkyard site can know that incoming HTTP requests are indeed meant for it.\n# (If the hostname is something else, Talkyard will instead reply that there is no such site.)\ntalkyard.hostname=\"localhost\"\n\n# If testing with Vagrant on localhost.\n#talkyard.port=8080\n\n# Set to true to use HTTPS.\ntalkyard.secure=false\n\n# Replace \"change_this\" with say 80 random characters. The value is secret.\n# The server will refuse to start until you've changed this.\nplay.http.secret.key=\"change_this\"\n\n\n# Email server\n# ---------------------\n\ntalkyard.smtp.host=\"\"\ntalkyard.smtp.user=\"\"\ntalkyard.smtp.password=\"\"\ntalkyard.smtp.fromAddress=\"support@your-organization.com\"\n\n## You can use STARTTLS:\ntalkyard.smtp.port=\"587\"\ntalkyard.smtp.requireStartTls=true\n## or connect directly with TLS:\n#talkyard.smtp.tlsPort=\"465\"\n#talkyard.smtp.connectWithTls=true\n## but don't try both at the same time.\n\n## If you're running your own email server, it needs a TLS certificate\n## for this to work. You can use LetsEncrypt to get a cert.\ntalkyard.smtp.checkServerIdentity=true\n\n\n# Spam detection\n# ---------------------\n\ntalkyard.googleApiKey=\"\"\ntalkyard.akismetApiKey=\"\"\ntalkyard.securityComplaintsEmailAddress=\"support@example.com\"\n\n\n# Backup and restore\n# ---------------------\n\n# How large SQL dump you can restore/import.\n# (Also see TY_NGX_LIMIT_REQ_BODY_SIZE in docker-compose.yml)\ntalkyard.maxImportDumpBytes=25100100\n\n# This is for importing additional Talkyard sites to this server. It would\n# then host many Talkyard sites, each one with its own unique hostname.\ntalkyard.mayImportSite=false\n\n\n# Other parts of the system\n# ---------------------\n\ntalkyard.redis.host=\"cache\"\ntalkyard.nginx.host=\"web\"\n\ntalkyard.postgresql.host=\"rdb\"\ntalkyard.postgresql.port=\"5432\"\ntalkyard.postgresql.database=\"talkyard\"\ntalkyard.postgresql.user=\"talkyard\"\n# Password in docker secret:  /run/secrets/postgres_password\n\n\n# Advanced\n# ---------------------\n\ntalkyard.featureFlags=\"\"\ntalkyard.maxGroupMentionNotifications=50\n\n#talkyard.createSiteHostname=\"\"\n#talkyard.cdnOrigin=\"//your-cdn.example.com\"\n\n\n# From environment variables\n# ---------------------\n# For changes to environment variables to take effect, the relevant container (typically\n# the app container) needs to be deleted (docker compose rm app) and recreated.\n\ntalkyard.becomeOwnerEmailAddress=${?BECOME_OWNER_EMAIL_ADDRESS}\ntalkyard.hostname=${?TALKYARD_HOSTNAME}\ntalkyard.port=${?TALKYARD_PORT}\ntalkyard.secure=${?TALKYARD_SECURE}\ntalkyard.createSiteHostname=${?CREATE_SITE_HOSTNAME}\ntalkyard.maintenanceApiSecret=${?TY_SYSMAINT_API_SECRET}\ntalkyard.emailWebhooksApiSecret=${?TY_EMAIL_WEBHOOKS_API_SECRET}\ntalkyard.featureFlags=${?TY_FEATURE_FLAGS}\nplay.http.secret.key=${?PLAY_SECRET_KEY}\n\n\n# Testing\n# ---------------------\n\ntalkyard.e2eTestPassword=${?E2E_TEST_PASSWORD}\ntalkyard.forbiddenPassword=${?FORBIDDEN_PASSWORD}\ntalkyard.mayFastForwardTime=${?MAY_FAST_FORWARD_TIME}\n\n\n# Authentication\n# ---------------------\n\n# OpenAuth login (i.e. login via Google, Facebook, etc).\ntalkyard.authn {\n\n  # Google provider\n  google.authorizationURL=\"https://accounts.google.com/o/oauth2/auth\"\n  google.accessTokenURL=\"https://accounts.google.com/o/oauth2/token\"\n  google.scope=\"profile email\"\n  #google.clientID=\"...\"\n  #google.clientSecret=\"...\"\n\n  # Facebook provider\n  facebook.authorizationURL=\"https://graph.facebook.com/v3.0/oauth/authorize\"\n  facebook.accessTokenURL=\"https://graph.facebook.com/v3.0/oauth/access_token\"\n  facebook.scope=\"email\"\n  #facebook.clientID=\"...\"\n  #facebook.clientSecret=\"...\"\n\n  # Twitter provider\n  twitter.requestTokenURL=\"https://twitter.com/oauth/request_token\"\n  twitter.accessTokenURL=\"https://twitter.com/oauth/access_token\"\n  twitter.authorizationURL=\"https://twitter.com/oauth/authenticate\"\n  #twitter.consumerKey=\"...\"\n  #twitter.consumerSecret=\"...\"\n\n  # GitHub\n  github.authorizationURL=\"https://github.com/login/oauth/authorize\"\n  github.accessTokenURL=\"https://github.com/login/oauth/access_token\"\n  github.scope=\"user:email\"\n  #github.clientID=\"...\"\n  #github.clientSecret=\"...\"\n\n}\n\n"
  },
  {
    "path": "conf/web/maint-msg.html",
    "content": "<html>\n<head>\n<title>Under Maintenance</title>\n<meta charset=\"utf-8\"/>\n<style>\nh1 { font-size: 33px; line-height: 150%; color: #444; }\nbody { font-size: 20px; text-align: center; padding: 50px 20px; color: #333; }\n</style>\n</head>\n<body>\n\n<h1>Wait 5 minutes</h1>\n\n<p>Then reload this page. We're upgrading the server.</p>\n\n</body>\n</html>\n\n"
  },
  {
    "path": "conf/web/sites-enabled/talkyard-servers.conf",
    "content": "## Nginx `server {}` blocks for your Talkyard forum.\n##\n## There's 1) a HTTP server, 2) a HTTPS server with auto generated LetsEncrypt certs.\n##\n## To redirect HTTP to HTTPS:\n## Comment out the 'include /etc/nginx/...' lines in the HTTP server (not the HTTPS server).\n## Comment in the 'return 302 ...' line.\n##\n## There's also 3) an out commented server block that shows how you can\n## add your own server with a custom cert, e.g. a wildcard cert.\n##\n## Note: If you comment-in/add-more server blocks, don't include 'backlog=8192'\n## in their listen directives — otherwise there'll be a \"duplicate listen options\"\n## Nginx error.  The backlog should be the same as net.core.somaxconn in /etc/sysctl.conf,\n## namely 8192, set in /opt/talkyard-v1/scripts/prepare-os.sh  [BACKLGSZ]\n## — but one may specify this in only one place.\n##\n\n\n## HTTP Server.\n## Replies to HTTPS cert challenges, can redirect to HTTPS.\n##\nserver {\n  listen 80      backlog=8192;   # about backlog: see above [BACKLGSZ]\n  ## Using ipv6 here, can prevent Nginx from starting, if the host OS has disabled ipv6,\n  ## Nginx then won't start and says:  [ipv6_probl]\n  #    [emerg] socket() [::]:80 failed (97: Address family not supported by protocol)\n  #listen [::]:80 backlog=8192;\n\n  server_name _;\n\n  ## For generating HTTPS certs via LetsEncrypt, HTTP-01 challenge.\n  location /.well-known/acme-challenge {\n    content_by_lua_block {\n      ngx.log(ngx.INFO, \"Replying to ACME HTTP-01 challenge\" ..\n              \", server name: \" ..  ngx.var.server_name ..\n              \", host: \" .. ngx.var.http_host .. \" [TyNACMEHTTP01]\")\n      require(\"resty.acme.autossl\").serve_http_challenge()\n    }\n  }\n\n  ## To redirect to HTTPS, comment out these two includes, and comment in\n  ## \"location / { return 302 ... }\" below.\n  include /etc/nginx/server-limits.conf;\n  include /etc/nginx/server-locations.conf;\n\n  ## Redirect from HTTP to HTTPS.\n  ## Use temp redirects (302) not permanent (301) in case you'll want to allow\n  ## http in the future, for some reason.\n  #location / {\n  #  return 302 https://$http_host$request_uri;\n  #}\n}\n\n\n## HTTPS Server with LetsEncrypt auto generated certs.\n##\nserver {\n  listen 443       ssl default_server backlog=8192;  # [BACKLGSZ]\n  #listen [::]:443 ssl default_server backlog=8192;  # [ipv6_probl]\n  http2 on;\n\n  server_name _;\n\n  ## Required, or Nginx won't start. Gets used until we've gotten a LetsEncrypt cert\n  ## (sth like 10 seconds after first HTTPS request to the server addr).\n  ssl_certificate     /etc/nginx/generated/https-cert-self-signed-fallback.pem;\n  ssl_certificate_key /etc/nginx/generated/https-cert-self-signed-fallback.key;\n\n  ssl_certificate_by_lua_block {\n    require(\"resty.acme.autossl\").ssl_certificate()\n  }\n\n  ## For generating HTTPS certs via LetsEncrypt, TLS-ALPN-01 challenge\n  ## (which works over HTTPS, unlike the HTTP-01 challenge).\n  ## Disabled in nginx.conf, because experimental in the lua-resty-acme plugin.\n  #location /.well-known/acme-challenge {\n  #  content_by_lua_block {\n  #    ngx.log(ngx.INFO, \"Replying to ACME TLS-ALPN-01 challenge\")\n  #            -- Cannot access here?:\n  #            -- \", server name: \" ..  ngx.var.server_name ..  \" [TyNACMEALPN01]\")\n  #    require(\"resty.acme.autossl\").serve_tls_alpn_challenge()\n  #  }\n  #}\n\n  include /etc/nginx/server-ssl.conf;\n  include /etc/nginx/server-limits.conf;\n  include /etc/nginx/server-locations.conf;\n}\n\n\n## HTTPS Server with custom (e.g. wildcard) HTTPS cert\n## ----------------------------------------------------\n\n## Redirect port 80 to 443: (without generating any LetsEncrypt cert, not needed)\n##\n#server {\n#  listen 80;  #  backlog=8192;   # [BACKLGSZ]\n#  server_name ~^(.*)\\.example\\.com$;\n#  return 302 https://$http_host$request_uri;\n#}\n\n## HTTPS Server with custom cert\n## Docs: http://nginx.org/en/docs/http/configuring_https_servers.html\n##\n#server {\n#  ## Comment out 'backlog=...' if you also use a LetsEncrypt auto cert server (above).\n#  ## Nginx won't start if 'backlog=...' is present at two places.\n#  listen 443      ssl;   # backlog=8192;  # [BACKLGSZ]\n#  #listen [::]:443 ssl;  # backlog=8192;  # [ipv6_probl]\n#  http2 on;\n#\n#  server_name ~^(.*)\\.example\\.com$;\n#\n#  ssl_certificate         /etc/certbot/live/example.com-0001/fullchain.pem;\n#  ssl_certificate_key     /etc/certbot/live/example.com-0001/privkey.pem;\n#\n#  include /etc/nginx/server-ssl.conf;\n#  include /etc/nginx/server-limits.conf;\n#  include /etc/nginx/server-locations.conf;\n#}\n\n"
  },
  {
    "path": "debug.yml",
    "content": "# Opens ports to Docker containers so you can connect and debug.\n# Only reachable locally (since listens on 127.0.0.1), so, access via an SSH tunnel.\n\nservices:\n  app:\n    ports:\n      - '127.0.0.1:9000:9000' # Play Framework's HTTP listen port, for bypasssing Nginx (to debug)\n      - '127.0.0.1:9443:9443' # Play Framework's HTTPS port\n      - '127.0.0.1:9999:9999' # Java debugger port\n      - '127.0.0.1:3333:3333' # JMX\n\n  cache:\n    ports:\n      - '127.0.0.1:6379:6379'\n\n  rdb:\n    ports:\n      - '127.0.0.1:5432:5432'\n\n  search:\n    ports:\n      - '127.0.0.1:9200:9200'\n\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "# Talkyard, discussion forum software, v1. Production installation on a single server.\n#\n# Dockerfiles for the Docker images are in another Git repo:\n# https://github.com/debiki/talkyard, at: images/(service-name)/Dockerfile\n#\n# There's an image build script: https://github.com/debiki/talkyard/blob/main/Makefile,\n# the `prod-images` and `tag-and-push-latest-images` targets.\n\n# Docker Compose file spec:\n# https://github.com/compose-spec/compose-spec/blob/main/spec.md\n\n\n# You can override this in docker-compose.override.yml.\n# Volume names get prefixed by the COMPOSE_PROJECT_NAME = 'talkyard-v1_', from .env.\nvolumes:\n  web-generated:\n  pub-files:\n  priv-files:\n  redis-data:\n  pg-data:\n  es-data:\n  es-logs:\n\n# See: https://docs.docker.com/compose/how-tos/use-secrets/\nsecrets:\n  postgres_password:\n    file: ./secrets/postgres_password.txt\n  #backup_password:\n  #  file: ./secrets/backup_password.txt\n\n# The netw names get prefixed with the COMPOSE_PROJECT_NAME.\nnetworks:\n  # Frontend public network. Connects Nginx to the Internet.\n  fe_pub_net:\n    ipam:\n      config:\n        - subnet: ${FE_PUB_NET_SUBNET}\n  # So Nginx can talk to Redis and the app server.\n  fe_int_net:\n    internal: true\n    ipam:\n      config:\n        - subnet: ${FE_INT_NET_SUBNET}\n  # Backend network: the app server and various databases. Is an internal network,\n  # not directly connected to the Internet.\n  # See: https://docs.docker.com/reference/compose-file/networks/#internal\n  be_int_net:\n    internal: true\n    ipam:\n      config:\n        - subnet: ${BE_INT_NET_SUBNET}\n  # So the app server can connect to the egress proxy.\n  eg_int_net:\n    internal: true\n    ipam:\n      config:\n        - subnet: ${EG_INT_NET_SUBNET}\n  # So the egress proxy can access the Internet, e.g. to send webhooks or fetch link previews.\n  eg_pub_net:\n    ipam:\n      config:\n        - subnet: ${EG_PUB_NET_SUBNET}\n\n# See: https://github.com/compose-spec/compose-spec/blob/main/spec.md#logging\nx-logging: &default_logging\n  # This driver rotates logs, so won't run out of disk.\n  driver: local\n\n\nservices:\n  web:\n    image: ${DOCKER_REG_ORG}/talkyard-web:${PINNED_VERSION_TAG:-${VERSION_TAG}}\n    # dockerfile: https://github.com/debiki/talkyard/blob/main/images/web/Dockerfile\n    restart: always\n    volumes:\n      # Nginx `server {...}` blocks for your Talkyard forum.\n      - ./conf/web/sites-enabled:/etc/nginx/sites-enabled:ro\n      # A LetsEncrypt ACME account key gets generated on startup and saved here.\n      # Once done, you could make this volume read-only: append ':ro' to the next line.\n      - web-generated:/etc/nginx/generated\n      # Don't, however, mount priv-files here.\n      - pub-files:/var/talkyard/v1/pub-files:ro\n\n      # Optionally, add a wildcard cert volume here:\n      # talkyard-wildcard-certs:/etc/wildcard-certs:ro\n      # Fetch certs in a Docker container using e.g. LEGO https://github.com/go-acme/lego\n      # that mounts the same volume, run in a cron job,\n      # and edit:  ./conf/web/sites-enabled/talkyard-servers.conf\n      # add e.g.:  ssl_certificate /etc/wildcard-certs/...something.crt;\n      #      and:  ssl_certificate_key ...\n\n    ports:\n      - '80:80'\n      - '443:443'\n    networks:\n      fe_pub_net:\n        ipv4_address: ${FE_PUB_NET_WEB_IP}\n      fe_int_net:\n        ipv4_address: ${FE_INT_NET_WEB_IP}\n    depends_on:\n      - app\n      - cache\n    logging: *default_logging\n    environment:\n      TY_NGX_ERROR_LOG_LEVEL: 'info'  # or 'notice' or 'debug'\n      # TY_NGX_ACCESS_LOG_CONFIG: 'tyalogfmt'\n      ## Max uploaded file size, e.g. uploaded images or backups to restore:\n      # TY_NGX_LIMIT_REQ_BODY_SIZE: '25m'\n      ## To let any CDN of yours bypass Ty's Nginx rate limits:\n      # X_PULL_KEY: '...'\n      # CDN_PULL_KEY: '...'\n\n    security_opt:\n      - no-new-privileges=true\n    # When cap_drop is ALL, it gets processed before cap_add, see:\n    # https://stackoverflow.com/a/63219871\n    # and:\n    # https://github.com/moby/moby/blob/1c39b1c44c973f18f39bd684c6aba57bb96510fe/oci/caps/utils.go#L120\n    cap_drop:\n      - ALL\n    cap_add:\n      # Without CHOWN:\n      # nginx: [emerg] chown(\"/opt/nginx/proxy-cache\", 100) failed (1: Operation not permitted)\n      - CHOWN\n      # To bypass file read, write, and execute permission checks:\n      # (DAC means \"discretionary access control\", and DAC_OVERRIDE)\n      # Without DAC_OVERRIDE:\n      # nginx: [emerg] mkdir() \"/var/cache/nginx/client_temp\" failed (13: Permission denied)\n      - DAC_OVERRIDE\n      # To change the group and user id of a process: (so Nginx won't need to run as root)\n      - SETGID\n      - SETUID\n      # To bind to lower ports. Maybe listen on 8080 instead, and map port 80:8080?\n      - NET_BIND_SERVICE\n\n  app:\n    image: ${DOCKER_REG_ORG}/talkyard-app:${PINNED_VERSION_TAG:-${VERSION_TAG}}\n    # dockerfile: https://github.com/debiki/talkyard/blob/main/images/app/Dockerfile.prod\n    restart: always\n    stdin_open: true  # otherwise Play Framework exits\n    volumes:\n      - ./conf/app/play-framework.conf:/opt/talkyard/app/conf/app-prod-override.conf:ro  # see [4WDKPU2] in github.com/debiki/talkyard\n      - pub-files:/var/talkyard/v1/pub-files\n      - priv-files:/var/talkyard/v1/priv-files\n      # So backups can be downloaded via the admin web interface. But read-only,\n      # so evil bugs cannot destroy all backups.\n      # - /var/opt/backups/talkyard/v1/:/var/opt/backups/talkyard/v1/:ro\n    secrets:\n      - postgres_password\n    networks:\n      fe_int_net:\n        ipv4_address: ${FE_INT_NET_APP_IP}\n      be_int_net:\n        ipv4_address: ${BE_INT_NET_APP_IP}\n      eg_int_net:\n        ipv4_address: ${EG_INT_NET_APP_IP}\n    depends_on:\n      # Start also if other services are unhealthy, so can show admin troubleshooting tips.\n      - rendr\n      - cache\n      - rdb\n      - search\n      - egressp\n    logging: *default_logging\n    environment:\n      # The  `TALKYARD_SECURE: '${TALKYARD_SECURE}'` syntax doesn't work — results in\n      # the env vars getting set to an empty string, but Play Framework wants a bool\n      # and refuses to start.\n      - TALKYARD_SECURE\n      # The app always looks here, not configurable. [postgres_pw_path]\n      # POSTGRES_PASSWORD_FILE=/tmp/postgres_password\n      - TALKYARD_HOSTNAME\n      - BECOME_OWNER_EMAIL_ADDRESS\n      # [egressp_conf]\n      - 'http_proxy=http://egressp:4750'\n      - 'https_proxy=http://egressp:4750'\n      - \"no_proxy=''\"\n    security_opt:\n      - no-new-privileges=true\n    cap_drop:\n      - ALL\n    cap_add:\n      - CHOWN   # to make secrets readable to 'appuser'\n      - FOWNER  #\n      - DAC_OVERRIDE\n      - SETUID  # to switch to 'appuser'\n      - SETGID  #              'appgroup'\n\n  rendr:\n    image: ${DOCKER_REG_ORG}/talkyard-rendr:${PINNED_VERSION_TAG:-${VERSION_TAG}}\n    # dockerfile: https://github.com/debiki/talkyard/blob/main/images/rendr/Dockerfile\n    networks:\n      be_int_net:\n        ipv4_address: ${BE_INT_NET_RENDR_IP}\n    logging: *default_logging\n    security_opt:\n      - no-new-privileges=true\n    cap_drop:\n      - ALL\n\n  cache:\n    image: ${DOCKER_REG_ORG}/talkyard-cache:${PINNED_VERSION_TAG:-${VERSION_TAG}}\n    # dockerfile: https://github.com/debiki/talkyard/blob/main/images/cache/Dockerfile\n    restart: always\n    volumes:\n      - redis-data:/data\n    networks:\n      fe_int_net:\n        ipv4_address: ${FE_INT_NET_CACHE_IP}\n    logging: *default_logging\n    sysctls:\n      net.core.somaxconn: 511\n    security_opt:\n      - no-new-privileges=true\n    cap_drop:\n      - ALL\n    cap_add: # [enough_caps]? See: https://github.com/docker-library/postgres/issues/649\n      - CHOWN  # [redis_cap_chown]\n      - SETGID\n      - SETUID\n      - SETPCAP\n\n  rdb:\n    image: ${DOCKER_REG_ORG}/talkyard-rdb:${PINNED_VERSION_TAG:-${VERSION_TAG}}\n    # dockerfile: https://github.com/debiki/talkyard/blob/main/images/rdb/Dockerfile\n    restart: always\n    secrets:\n      - postgres_password\n    volumes:\n      - pg-data:/var/lib/postgresql\n    networks:\n      be_int_net:\n        ipv4_address: ${BE_INT_NET_RDB_IP}\n    logging: *default_logging\n    environment:\n      POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password\n    security_opt:\n      - no-new-privileges=true\n    cap_drop:\n      - ALL\n    cap_add: # [enough_caps]? See: https://github.com/docker-library/postgres/issues/649\n      - CHOWN\n      - DAC_READ_SEARCH\n      - DAC_OVERRIDE  # to copy Postgres secret\n      - FOWNER\n      - SETGID\n      - SETUID\n      - SYS_NICE\n\n  search:\n    image: ${DOCKER_REG_ORG}/talkyard-search:${PINNED_VERSION_TAG:-${VERSION_TAG}}\n    # dockerfile: https://github.com/debiki/talkyard/blob/main/images/search/Dockerfile\n    restart: always\n    volumes:\n      - es-data:/usr/share/elasticsearch/data\n      # Deprecation logs aren't written to stdout, but to files in this dir,\n      # auto rotated, max 5G in total.\n      # See: https://www.elastic.co/docs/deploy-manage/monitor/logging-configuration/elasticsearch-deprecation-logs\n      - es-logs:/usr/share/elasticsearch/logs\n    networks:\n      be_int_net:\n        ipv4_address: ${BE_INT_NET_SEARCH_IP}\n    logging: *default_logging\n    environment:\n      bootstrap.memory_lock: 'true'\n      ES_JAVA_OPTS: '-Xms512m -Xmx512m'\n    ulimits:\n      memlock:\n        soft: -1\n        hard: -1\n      nofile:\n        soft: 65536\n        hard: 65536\n    security_opt:\n      - no-new-privileges=true\n    cap_drop:\n      - ALL\n    cap_add:  # [enough_caps]?\n      # SYS_CHROOT? See  https://github.com/nasa-jpl/ASSESS/blob/master/docker-compose.yml\n      - SETGID\n      - SETUID\n\n  # Egress proxy.\n  # Stops Server Side Request Forgery (SSRF).\n  egressp:\n    image: ${DOCKER_REG_ORG}/talkyard-egressp:${PINNED_VERSION_TAG:-${VERSION_TAG}}\n    restart: always\n    networks:\n      eg_int_net:\n        ipv4_address: ${EG_INT_NET_EGRESSP_IP}\n      eg_pub_net:\n        ipv4_address: ${EG_PUB_NET_EGRESSP_IP}\n    logging: *default_logging\n    read_only: true\n    security_opt:\n      - no-new-privileges=true\n    cap_drop:\n      - ALL\n\n  backup:\n    image: ${DOCKER_REG_ORG}/talkyard-backup:${PINNED_VERSION_TAG:-${VERSION_TAG}}\n    # dockerfile: https://github.com/debiki/talkyard/blob/main/images/backup/Dockerfile\n    profiles: [backup] # prevents auto-start on 'up'\n    # If under heavy load, run the backup script at 25% priority. (Default is 1024 shares.)\n    cpu_shares: 256\n    # Don't let the backup container use more than 60% CPU (even if the system is idle),\n    # otherwise on a small VM the kernel might panic [100_kernel_panic].\n    cpu_quota:   60000\n    cpu_period: 100000\n    secrets:\n      # Will appear at /run/secrets/postgres_password. [backup_pg_client_secret]\n      - postgres_password\n      #- backup_password\n    volumes:\n      # Here we save the backups.\n      - /var/opt/backups/talkyard/v1:/var/opt/backups/talkyard/v1\n      # So can backup uploaded files and Redis.\n      - pub-files:/var/talkyard/v1/pub-files:ro\n      - priv-files:/var/talkyard/v1/priv-files:ro\n      - redis-data:/var/talkyard/v1/redis-data:ro\n      # So can backup config files.\n      - .:/opt/talkyard-v1:ro\n    logging: *default_logging\n    depends_on:\n      # So can backup database.\n      - rdb\n    networks:\n      be_int_net:\n    security_opt:\n      - no-new-privileges=true\n    cap_drop:\n      - ALL\n    cap_add:\n      # So 'root' can access Redis' dump.rdb file, which is owned by user 'redis' 999.\n      - CAP_DAC_READ_SEARCH\n\n# vim: et ts=2 sw=2\n"
  },
  {
    "path": "docs/copy-backups-elsewhere.md",
    "content": "Take regular off-site backups\n======================\n\nAfter you've installed Talkyard, you should regularly copy the backups\nto an off-site backup server. Here's a way to do that.\n\n**Note:** Not yet tested in Talkyard v1, but should work — only the\nbackup archives path has changed (from `/opt/talkyard-backups/archives/`\nto `/var/opt/backups/talkyard/v1/archives/`).\n\n### Create SSH key\n\nOn the backup server, preferably located in another datacenter, create a SSH\nkey:\n\n    ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_remotebackup -C \"Automated remote backup\"\n\nYou can perhaps skip the passphrase, since the backup server will have\nread-only rsync access only, all backups will be available on the backup server\nanyway. So a passhprase doesn't give any additional security.\n\n\n### Enable restricted rsync, rrsync\n\nOn the Talkyard server, enable rrsync:\n\n    zcat /usr/share/doc/rsync/scripts/rrsync.gz > /usr/local/bin/rrsync\n    chmod ugo+x /usr/local/bin/rrsync\n\n\n### rsync keys\n\nThen create a backup user with an `authorized_keys` file that allows restricted rsync:\n\n    # (still on the Talkyard server)\n    useradd --create-home remotebackup\n    su - remotebackup\n    mkdir .ssh\n    echo 'command=\"/usr/local/bin/rrsync -ro /var/opt/backups/talkyard/v1/archives/\",no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding' >> .ssh/authorized_keys\n\nCopy the public key on the backup server:\n\n    # on the backup server:\n    cat ~/.ssh/id_remotebackup.pub\n\n    # copy the output\n\nAppend the public key to the last line in `authorized_keys` on the Talkyard server:\n\n    # as user remotebackup: (!)\n    nano ~/.ssh/authorized_keys\n\n    # append a space and then the stuff you just copied to the last line (which is the only line, if the file was just created).\n    # Do not paste it on a new line.\n\nThe result should be that the `authorized_keys` file looks like: (and it's a really long line)\n\n    command=..... ssh-rsa AAAA................ Automated remote backup\n\n\n### Test\n\nNow, on the backup server, test copying backups:\n\n    # replace 'SERVERADDRESS' with your Talkyard server address\n    rsync -e \"ssh -i $HOME/.ssh/id_remotebackup\" -av remotebackup@SERVERADDRESS:/ $HOME/talkyard-backups/\n\n\n### Schedule copying-of-backups\n\nIf the above test works, then schedule a cron job to copy backups regularly. Do this on the backup server:\n\n    # again, replace 'SERVERADDRESS' with your Talkyard server address\n    crontab -l | { cat; echo '@hourly rsync -e \"ssh -i .ssh/id_remotebackup\" -av remotebackup@SERVERADDRESS:/ talkyard-backups/ >> cron.log 2>&1'; } | crontab -\n\nNow you'll have fresh backups of your forum in ~/talkyard-backups/, in case the Talkyard\nserver disappears.\n\n(Why do we run the rsync client read-only on the backup server? Well, because\nif we were to let the Talkyard server connect and write to the backup server, then\nsomeone who breaks in to the Talkyard server could ransomware-encrypt all backups\n(that is, encrypt everything and tell you \"give me money, only then will I\nunencrypt your data so you can read it again\"). But when the Talkyard server doesn't\nhave access to the backup server, this cannot happen. Note that it should be\neasier to make the backup server safe, because it doesn't need to run the whole\nTalkyard tech stack.)\n\n\n### Get an email, if backups stop working\n\n*NOT YET IMPLEMENTED* ` [BADBKPEML]`, the following does not yet work:\n\nOn the remote backup server, copy the contents of the script\n[scripts/check-talkyard-backups.sh](../scripts/check-talkyard-backups.sh)\nto your home directory. Edit the script and fill in email server (SMTP)\ncredentials.\n\nThen, test run the script:\n\n    cd $HOME\n    ./check-talkyard-backups.sh --send-email-if-bad talkyard-backups/\n\nAnd test send an email:\n\n    ./check-talkyard-backups.sh --send-test-email\n\nIf seems to work, run daily via Cron:\n\n    crontab -l | { cat; echo '@daily ./check-talkyard-backups.sh --send-email-if-bad talkyard-backups/ >> cron.log 2>&1'; } | crontab -\n\n"
  },
  {
    "path": "docs/how-restore-backup.md",
    "content": "\nHow restore a Talkyard backup\n=============================================\n\n**Notice:** Not yet updated for Talkyard v1,\nfor example, steps for decrypting encrypted backups are missing.\n\nIf your server disappeared, and you want to restore a backup on a new server.\nOr if you're upgrading from Talkyard epoch v0 to v1. Then you can do as follows.\n\nStart installing Talkyard on that new server, following the instructions in\nhttps://github.com/debiki/talkyard-prod-one/blob/ty-prod-one-v1/README.md\n— but stop at step 8: \"Edit config values\".\nInstead, we'll copy config files from the backup:\n\nOn the new server, as root, run the commands below:\n\nReplace `BACKUP_ARCHIVES_DIR` and `DB_BACKUP_FILE` etcetera below, with\nthe actual path and file names.\n\n```\nsudo -i # become root\ncd /opt/talkyard-v1\n\necho \"$(date -I): Restoring backup ...\" >> talkyard-maint.log\n\n\n# Restore config files and HTTPS certs\n# ------------------------------\n\n# First, let's \"backup\" the new conf, in case you'd like to diff old vs default.\nmkdir -p default-conf/data\nmv  conf  docker-compose.*  .env  default-conf/\nmv  data/certbot  data/sites-enabled-auto-gen  default-conf/data/\n\n# Then restore the old config.\nmkdir old-conf\nmkdir data\ntar xf /BACKUP_ARCHIVES_DIR/CONFIG_BACKUP_FILE.tar.gz -C old-conf\nmv old-conf/.env                        ./\nmv old-conf/docker-compose.*            ./\nmv old-conf/conf                        ./conf\n\n\n# Stop any App server\n# ------------------------------\n\n# This shouldn't be needed — you didn't start the Talkyard server yet?\n# You stopped at step 8 as mentioned above?\n# Anyway, if the Talkyard app server is running, stop it:\n# (Otherwise the restore will fail because of active database connections.)\ndocker compose stop app\n\n\n# Restore the database, PostgreSQL\n# ------------------------------\n\n# First, start PostgreSQL.\ndocker compose up -d rdb\n\n# NOTE: Overwrites any existing database (!).\nzcat /BACKUP_ARCHIVES_DIR/DB_BACKUP_FILE.sql.gz \\\n    | docker exec -i $(docker compose ps -q rdb) psql postgres postgres \\\n    | tee -a talkyard-maint.log\n\n\n# Restore Redis?\n# ------------------------------\n\n# Not needed, it's a cache. (Maybe write something about Redis later.)\n\n\n# Restore uploaded files\n# ------------------------------\n\n# Test if works!  [ty_v1]\ndocker compose run --rm  \\\n    -v /BACKUP_ARCHIVES_DIR/UPLOADS_BACKUP_DIR.d:/uploads:ro  \\\n    app \\\n        rsync -a  /uploads/  /var/talkyard/v1/pub-files/uploads/\n```\n\n### Memory\n\nNext, configure memory: Run `free -m` to find out how many megabytes\nmemory your machine has. Look at docker-compose.override.yml to see how\nmuch memory Talkyard has been configure to use — and optionally,\nreplace that file with another more suitable one from `./mem/*`,\ne.g.: `cp mem/4g.yml docker-compose.override.yml`.\n\n\n### Start Talkyard\n\nNow, time to start everything:\n\n```\ndocker compose up -d\ndocker compose logs -f --tail 999\n```\n\nAlso, think about if you need to 1) update your DNS server with the IP address to\nyour new Talkyard server. Or maybe 2) change the hostname of the Talkyard server\n— you'd then edit Nginx config in `conf/app/play-framework.conf`,\nand `conf/web/sites-enabled/talkyard-servers.conf`, plus\ngenerate a LetsEncrypt cert\n(see: `https://github.com/debiki/talkyard-prod-one/blob/ty-prod-one-v1/docs/setup-https.md`).\n\n\n### Backups and automatic upgrades\n\nContinue with step ?? in the installation instructions in README.md,\nhttps://github.com/debiki/talkyard-prod-one/blob/ty-prod-one-v1/README.md,\nthat is, this step:\n*\"Schedule deletion of old log files, daily backups and deletion old backups, and automatic upgrades\"*.\n\nAlso, look at the *Next steps* just below, in README.md — you'll want to configure off-site backups?\n"
  },
  {
    "path": "docs/multisite-talkyard.adoc",
    "content": "\nMultisite Talkyard\n======================================================================\n\n\nOne Talkyard server can host many Talkyard sites:\ndifferent forums and blogs, with different owners and admins.\n\nIf you've installed Talkyard as usual, for a single site,\nthen, you can enable Multisite Talkyard.\nLet's say your main Talkyard site is at: `main-talkyard.example.com`. Then,\n\n. Add DNS record(s) for the new sites you'll create.\nCould be a wildcard A or CNAME record:\n\n```\nmain-talkyard.example.com  3600  IN  A  11.22.33.44\n*.multi-ty.example.com     3600  IN  A  main-talkyard.example.com.\n```\n\nIn `/opt/talkyard/conf/play-framework.conf`,\nscroll down to the Advanced section; add these settings:\n\n```\ntalkyard.createSiteHostname=\"main-talkyard.example.com\"\ntalkyard.baseDomain=\"multi-ty.example.com\"\n```\n\nwhere `main-talkyard.example.com` is the address to your already working Talkyard site,\nthe firt site you installed.\n\nNow, you can go to: `https://main-talkyard.example.com/-/create-site`, or\n`https://main-talkyard.example.com/-/create-site/blog-comments`,\nand create a new Talkyard site.\n\nIts address will be: `https://something.multi-ty.example.com`.\n\nHTTPS should work automatically — Talkyard and LetsEncrypt generates certs for you.\nOnly the very first time someone (you) accesses a new site,\nthere'll be a connection-not-secure error,\nand you'll need to wait 10 – 20 seconds and reload the page.\n\nOnce you've created an additional Talkyard site,\nyou can, if you want to, change its address to something else,\nby going here:  `https://something.multi-ty.example.com/-/admin/settings/site`\nand clicking *Change address*.\n"
  },
  {
    "path": "docs/release-channels.md",
    "content": "\nRelease Channels\n======================================================================\n\nLater, you can choose between:\n1. Getting new features and bug fixes more often — the `regular` release branch.\n2. Getting only important bug fixes — the `lts` (Long Term Stable) branch.\n\nYou choose this by editing `/opt/talkyard-v1/.env`\nand setting `RELEASE_BRANCH=regular` or `...=lts`.\n\nCurrently only the `regular` branch exists.\nIt's the default, so you don't need to do anything.\n\n(This is inspired by Kubernetes' release channels:\nhttps://cloud.google.com/kubernetes-engine/docs/concepts/release-channels)\n\n"
  },
  {
    "path": "docs/risk-free-upgrades.md",
    "content": "Risk-Free Upgrades (Blue-Green, Google Cloud)\n=========================\n\nYou can use this method for major OS upgrades or major Talkyard migrations, e.g. v0 to v1.\n\nFor routine updates, we recommend just following the installation instructions,\nthat is, a daily cron job that calls `scripts/upgrade-if-needed.sh`.\n\n**Important:** Read/skim all steps before you start.\nEspecially read the **Make backups work again**\nand **Rolling back** sections at the end.\n\n<!--\nIf you use Google Cloud or Amazon AWS, you can upgrade your Talkyard server without any downtime.\nIt'll be read-only during the upgrade.\nThis works for other software too, not just Talkyard. For example, to upgrade the\nserver OS from Debian 11 to 12.\n-->\n\n\n<!--  Wow so much text I wrote!\nNot needed\n-------------------------\n\nWe don't recommend doing this, because usually it's not worth the trouble.\nIt's simpler to just let `scripts/upgrade-if-needed.sh` run once a day,\nand accept one or two minutes downtime once a month or something like that.\n\n(After all, Amazon and CloudFlare have had many hours or almost a day's downtime recently.)\n\nBut if you're migrating from Talkyard v0 to v1, or you're upgrading the Operating System,\nthen this zero-downtime approach makes more sense, because if there's any problem,\nyou can just _not_ point the IP address to the new server (or point it back to the old),\nand your end users won't notice anything. (See below.)\n\nHow does it work?\n-------------------------\n\nThis makes use of multi-disk crash-consistent machine images, which apparently\nno other cloud providers than Google Cloud and AWS supports.\n-->\n\n\nPrerequisites\n-------------------------\n\n<!-- Need not mention: A Talkyard forum, and a public static IP — too obvious,\notherwise there's nothing to upgrade and they wouldn't be reading this.  -->\n\n- These docs are for Google Cloud. (No docs written for AWS or anything else.)\n\n- Familiarity with Google Cloud, e.g. how to SSH into a VM.\n\n- SSH access to your off-site backup server, if any.\n\n- Know how to edit your hosts file (on your laptop).\n\n- Know how to open your browser's Dev Tools and switch to the Network tab to check\n  IP addresses.\n\n\nInstructions\n-------------------------\n\n1. **Before**\n\n   1. **Enable Maintenance Mode** which also makes the server read-only.\n      SSH into your server, and:\n\n      ```\n      cd /opt/talkyard\n      sudo docker-compose exec rdb psql talkyard talkyard -c \\\n            'update system_settings_t set maintenance_until_unix_secs_c = 1;'\n      ```\n\n      Now the forum should show an Under Maintenance message:\n\n      (screenshot)\n\n1. **Clone the server**\n   1. In Google Cloud, go to **Virtual Machines > VM Instances**, and find your VM.\n\n   1. Click the three dots **⋮** next to your VM, then click **Create new machine image**.\n\n       (screenshot)\n\n   1. Once created, go to **Machine Images**, find the new image,\n      click **⋮** then click **Create instance**.\n\n   1. Launch the VM in same region and zone as the old VM, with the same machine type.\n\n1. **Verify & Upgrade**\n\n   1. Let's see if the new VM works. Find the IP of the new VM, and\n      add it to your laptop's `/etc/hosts`:\n\n      ```\n      11.22.33.44  your-forum.example.com\n      ```\n\n   1. In a browser, go to `https://your-forum.example.com`\n      and open Dev Tools. Switch to the Network tab. Verify that you're\n      hitting the IP of the new VM (and not the _old_ VM).\n\n   1. **Upgrade.** SSH into the new VM. Upgrade the forum or the OS.\n\n   1. Do some manual testing in the browser, including:\n      - Visit: `your-forum.example.com/-/build-info`\n        do you see the new (upgraded) Talkyard version number?\n      - Post a test topic in a hidden category (e.g. staff-only).\n      - Visit:  `your-forum.example.com/-/last-errors` — you see any errors?\n\n1. **Switch Traffic**\n\n   1. Move your forum's IP address from the old VM to the new VM:\n      Go to **VPC Network > IP addresses**, find the forum's public IP,\n      click **⋮** and **Reassign to another resource**, select the new VM.\n\n   1. Remove the `/etc/hosts` entry (on your laptop).\n      Reload web page, look in Dev Tools, the Network tab, and verify you're\n      hitting the public IP address of the forum — which now points to the _new_ VM.\n\n1. **Afterwards**\n\n    1. **Disable Maintenance Mode.** On the new VM:\n\n       ```\n       cd /opt/talkyard\n       sudo docker-compose exec rdb psql talkyard talkyard -c \\\n             'update system_settings_t set maintenance_until_unix_secs_c = null;'\n       ```\n\n       Reload the web page. Did the maintenance message disappear?\n\n   1. **Shut down** the old VM, but don't delete it (wait a month).\n\n      Reload the web page — still works?\n\n   <!-- Network tags like  https-server,  http-server  should carry over,\n   Gemini 3 Fast says. Don't think it's worth mentioning, since will just work,\n   and organizations with any complex firewall network should have their\n   own routines & knowledge anyway.  -->\n\n1. **Make backups work again**\n\n   If you copy backups off-site using rsync (as described in `copy-backups-elsewhere.md`),\n   this now fails with a _\"REMOTE HOST IDENTIFICATION HAS CHANGED\"_ warning,\n   because Google Cloud will have given the new VM a different SSH host key\n   (even though it's a machine image of the old). Therefore:\n\n   1. SSH into the remote backup server. Remove the old key, accept the new:\n\n      ```\n      ssh-keygen -R your-forum.example.com  # removes old key\n      ssh your-forum.example.com            # accepts new. Type 'yes' when prompted\n      ```\n\n   1. Type `crontab -l` to list cron jobs (on the remote backup server).\n      Copy-paste the line that `rsync`s the Talkyard backups,\n      and run it manually, verify works fine.\n\n   Also, make sure you've configured Google Cloud to backup the new VM regularly\n   (if you want to do that in addition to Talkyard's own backups).\n\n### Rolling back\n\nIf there's any problem, either don't reassign the IP, or move it back to the old VM.\nDisable Maintenance Mode on the **old** VM using the SQL command from the\n_Disable Maintenance Mode_ step above.\n\nYou can figure out what went wrong on the new VM without any stress.\n(Unless, of course, something bad happens _after_ you've completely migrated to\nthe new VM. That's why it's good to run some tests before switching over.)\n\n\nRelated reading\n-------------------------\n\n- About machine images:\n    https://cloud.google.com/compute/docs/machine-images#disk-backup\n- Reassigning external IP addresses:\n    https://cloud.google.com/compute/docs/ip-addresses/reserve-static-external-ip-address#IP_assign\n- Reassigning an external IP programatically:\n    - https://cloud.google.com/sdk/gcloud/reference/compute/instances/delete-access-config\n    - https://cloud.google.com/sdk/gcloud/reference/compute/instances/add-access-config\n\n"
  },
  {
    "path": "docs/troubleshooting.md",
    "content": "\nTroubleshooting Talkyard\n========================\n\nInstallation Problems\n---------------------\n\n\n### Error: talkyard_web_1, Read timed out\n\nIf, when you run:\n\n    /scripts/upgrade-if-needed.sh 2>&1 | tee -a talkyard-maint.log\n\nyou're getting this error:\n\n    Creating talkyard_web_1    ...\n\n    ERROR: for talkyard_web_1  UnixHTTPConnectionPool(host='localhost', port=None): Read timed out. (read timeout=...)\n\n    ERROR: for web  UnixHTTPConnectionPool(host='localhost', port=None): Read timed out. (read timeout=...)\n    An HTTP request took too long to complete. Retry with --verbose to obtain debug information.\n    If you encounter this issue regularly because of slow network conditions, consider setting COMPOSE_HTTP_TIMEOUT to a higher value (current value: 240).\n\nthen the reason can be that the server has too little memory — which apparently can cause\nNginx (OpenResty) to run out of memory and crash. Now you might wonder, why would Nginx use\nthat much memory? — I think it's OpenResty (an Nginx distribution) that just-in-time compiles\nlots of Lua code, and then uses lots of memory.\n\n\n\nOld: Troubleshooting and debugging\n----------------\n\n(Ignore this section; it's not completed and hard to understand.)\n\n? save Java crash dumps in ./play-crash\n+ tips about how to run jmap? or view in jvisualvm + Idea? jmap -heap PID\n\nHow to connect VisualVM\n\nTips about how to view logs: all logs, app specific logs.\n\nHow to jump into a Docker container.\n\nHow to connect a debugger: open Docker port, then connect via SSH tunnel (assuming a firewall blocks the port on the host).\nIf using Google Compute Engine, then ssh tunnel:\n\n    gcloud compute ssh server-name --ssh-flag=-L9999:127.0.0.1:9999 --ssh-flag=-N\n\n\nHow to open console in Chrome, view messages & post to the E.D. help forum.\n\nView CPU & memory usage: `./scripts/stats.sh`\n\n\n"
  },
  {
    "path": "docs/upgrade-v0-to-v1.md",
    "content": "Upgrading from Talkyard v0 to v1\n================================\n\n**NOTICE**: This is a **draft**, not yet finished or tested.\n\n---\n\nTalkyrad v1 is a major new version of Talkyard — a new epoch.\nPrevious versions have been v0.YYYY.NNN,\nnewer versions will be v1.YYYY.NNN (e.g. v1.2025.001).\n\nTo upgrade, you'll install Talkyrad v1 side-by-side with v0, backup v0,\nshut down v0, restore the backup to v1, and start v1.\n\n\nWhy upgrade?\n-------------------------\n\n- Talkyard v1 upgrades all components to more recent versions\n  (upgrades to PostgreSQL 18, ElasticSearch 9 (or 8), Redis 8, Debian 12 or 13).\n  This is good to do, so you'll be using supported versions of the software.\n\n- Improvements to the maintenance scripts, e.g. optionally encrypted backups.\n\n- We'll start using Docker named volumes, instead of bind mounts.\n\n- We'll start using the Linux Filesystem Hierarch Standard, e.g.\n  backups in `/var/opt/backups/talkyard/` instead of `/opt/talkyard-backups/`.\n\n- Talkyard v0 will stop getting new features, only bug fixes.\n\n\nHow to upgrade\n-------------------------\n\n### Preparations\n\nUpgrade your Operating System to Debian 12 or 13\n(Ubuntu 22 or 24 LTS should work too — they're based on Debian 12 and 13).\n\nUpgrade Docker to >= ???. Install Docker Compose v2, if you haven't already:\n\n    apt-get install docker-compose-plugin\n\n(Talkyard v0 uses Docker Compose v1, but Talkyard v1 uses Docker Compose v2.)\n\n\n### Phase 1: Backup and shut down v0\n\nMake your Talkyard v0 server read-only:\n\n```\ncd /opt/talkyard   # this is v0\n\ndocker-compose exec rdb psql ...\n```\n\nTake a backup, let's name it `beforeV1Upgrade`:\n\n```\n./scripts/backup.sh beforeV1Upgrade\n```\n\nNow, shut down Talkyrad v0 — but don't delete anything! Just leave it as-is:\n\n```\ndocker-compose down   # (using Docker-Compose v1)\n```\n\n#### Verification\n\nOpen a web browser and check that you the Talkyard site is inaccessible.\n\n\n### Phase 2: Install and configure v1\n\n\nInstall Talkyard v1, as per the installation instructions in ../README.md .\n\nCopy configuration files from your v0 installation to v1 — with one change:\n\n```\n# Go to the Talkyard v1 installation dir\ncd /opt/talkyard-v1\n\n# Back up default config\nmv conf conf.v1.default\n\n# Copy config from v0 to v1:\ncp -a /opt/talkyard/conf  ./\n\n# Move play-framework.conf to 'app/' sub dir: (for consistency with other containers)\nmkdir conf/app\nmv conf/play-framework.conf conf/app/play-framework.conf\n```\n\nEdit the config files:\n\n```\n... TBD ...\n```\n\n\n#### Verification\n\nStart the new Talkyard v1 site:\n\n```\ncd /opt/talkyard-v1\ndocker compose up -d\n```\n\nSee if you can access it in a browser. It'll be empty, since you haven't restored the\ndatabase yet.\n\n\n### Phase 3: Data migration\n\n#### Restore database\n\nRestore the database backup into Talkyrad v1: (and replace `...beforeV1Upgrade...sql.g`\nwith the backup file name.)\n\n```\ncd /opt/talkyard-v1/      # note: v1\ndocker compose up -d rdb  # (this is Docker Compose v2)\n\n# NOTE: Overwrites any existing database (!).\nzcat /opt/talkyard-backups/archives/...beforeV1Upgrade...sql.gz \\\n    | docker exec -i $(docker compose ps -q rdb) psql postgres postgres \\\n    | tee -a talkyard-maint.log\n```\n\n#### Copy uploaded files\n\nCopy uploaded files from v0 (located at `/opt/talkyard/data/uploads`)\ninto the v1 named volume, using a temporary 'app' container. This container\nwhich automatically mounts the volume, as specified in docker-compose.yml.\n\n```\n# In /opt/talkyard-v1:\n\ndocker compose run --rm  \\\n    -v /opt/talkyard/data/uploads:/uploads-v0:ro  \\\n    app \\\n        rsync -a  /uploads-v0/public/  /var/talkyard/v1/pub-files/uploads/\n```\n\n\n#### Does it work?\n\nStart Talkyard v1 with your data restored:\n\n```\n# In /opt/talkyard-v1:\n\ndocker compose up -d\ndocker compose logs -f\n```\n\nOpen a web browser and see if you can access your Talkyard site again.\n\n\n### Phase 4: Last steps\n\n#### Make the site read-write\n\n```\ndocker compose exec rdb psql ...\n```\n\n\n#### Reconfigure backups\n\nReconfigure the off-site backup script so it backups `/var/opt/backups/`\n(instead of `/opt/talkyard-backups/`) — see the end of ../README.md.\n\nWait a month or two. All fine? You can delete old v0.\n\n\n"
  },
  {
    "path": "mem/1.7g.yml",
    "content": "# For servers with 1.7 GB RAM.\n# E.g. a Google Compute Engine g1-small instance: 1 shared CPU & 1.7 GB mem.\n\n# (We don't make use of all RAM here, because ElasticSearch and Postgres wants\n# fairly much memory in the operating system file system cache, e.g.\n# ElasticSearch wants as much mem for the OS cache as for its own heap.\n# Also, there're Nginx and Redis containers too.)\n\nservices:\n  app:\n    environment:\n      # There's also stack memory and permanent-generation memory.\n      PLAY_HEAP_MEMORY_MB: 256\n\n  search:\n    environment:\n      ES_JAVA_OPTS: '-Xms192m -Xmx192m'\n    deploy:\n      resources:\n        limits:\n          memory: 0.9G\n\n\n"
  },
  {
    "path": "mem/2g.yml",
    "content": "# For servers with 2 GB RAM.\n\n# (We don't make use of all RAM here, because ElasticSearch and Postgres wants\n# fairly much memory in the operating system file system cache, e.g.\n# ElasticSearch wants as much mem for the OS cache as for its own heap.\n# Also, there're Nginx and Redis containers too.)\n\nservices:\n  app:\n    environment:\n      # There's also stack memory and permanent-generation memory.\n      PLAY_HEAP_MEMORY_MB: 512\n\n  search:\n    environment:\n      ES_JAVA_OPTS: '-Xms320m -Xmx320m'\n    deploy:\n      resources:\n        limits:\n          memory: 1G\n\n"
  },
  {
    "path": "mem/4g.yml",
    "content": "# For servers with 4 GB RAM.\n\n# (We don't make use of all RAM here, because ElasticSearch and Postgres wants\n# fairly much memory in the operating system file system cache, e.g.\n# ElasticSearch wants as much mem for the OS cache as for its own heap.\n# Also, there're Nginx and Redis containers too.)\n\nservices:\n  app:\n    environment:\n      # There's also stack memory and permanent-generation memory.\n      PLAY_HEAP_MEMORY_MB: 1024\n\n  search:\n    environment:\n      ES_JAVA_OPTS: '-Xms512m -Xmx512m'\n    deploy:\n      # Let's restrict the container to 40% of total mem.\n      resources:\n        limits:\n          memory: 1.6G\n\n"
  },
  {
    "path": "mem/7.5G.yml",
    "content": "# For servers with 7.5 GB RAM.\n\n# (We don't make use of all RAM here, because ElasticSearch and Postgres wants\n# fairly much memory in the operating system file system cache, e.g.\n# ElasticSearch wants as much mem for the OS cache as for its own heap.\n# Also, there's a Nginx and a Redis container too.)\n\nservices:\n  app:\n    environment:\n      # There's also stack memory and permanent-generation memory.\n      PLAY_HEAP_MEMORY_MB: 1300\n\n  search:\n    environment:\n      ES_JAVA_OPTS: '-Xms1100m -Xmx1100m'\n    deploy:\n      # Let's restrict the container to 40% of total mem.\n      resources:\n        limits:\n          memory: 3G\n\n"
  },
  {
    "path": "mem/8G.yml",
    "content": "# For servers with 8 GB RAM.\n\n# (We don't make use of all RAM here, because ElasticSearch and Postgres wants\n# fairly much memory in the operating system file system cache, e.g.\n# ElasticSearch wants as much mem for the OS cache as for its own heap.\n# Also, there's a Nginx and a Redis container too.)\n\nservices:\n  app:\n    environment:\n      # There's also stack memory and permanent-generation memory.\n      PLAY_HEAP_MEMORY_MB: 1800\n\n  search:\n    environment:\n      ES_JAVA_OPTS: '-Xms1800m -Xmx1800m'\n    deploy:\n      # Let's restrict the container to 40% of total mem.\n      resources:\n        limits:\n          memory: 3.2G\n\n"
  },
  {
    "path": "scripts/backup.sh",
    "content": "#!/bin/bash\n\nlabel=\"${1:-manual}\"\n\nexec /usr/bin/docker compose run --rm backup  \\\n      /ty/backup.sh  \"$label\"  \"$(hostname)\"  \"$(date '+%FT%H%MZ' --utc)\"\n\n\n"
  },
  {
    "path": "scripts/delete-old-backups.sh",
    "content": "#!/bin/bash\n\nexec /usr/bin/docker compose run --rm backup  \\\n      /ty/delete-old-backups.sh\n\n"
  },
  {
    "path": "scripts/find-admin-login-link.sh",
    "content": "#!/bin/bash\n\n# This prints admin one-time login links, and admin reset password links,\n# generated via:  http://ty-server/-/admin-login\n# or via the Reset Password buttons.\n#\n# This is useful, if email hasn't yet been configured. Then one\n# can login as root, and run this script instead.\n#\n# Sync with this: [GETADMLNK] and [RSTPWDLNK].\n\ndc=\"/usr/bin/docker compose\"\n\ndb_user=\"$1\"\n\nif [ -z \"$db_user\" ]; then\n  db_user=\"talkyard\"\nfi\n\n# Set  pager=off  otherwise psql prints \"More...\" and waits for you to\n# hit Space.\npsql=\"psql -P pager=off $db_user $db_user\"\n\n\n# Print admin emails, in case one doesn't remember one's admin email — e.g.\n# after migrating from Talkyard.net to self hosted:\n\nadmin_addrs=$($dc exec rdb $psql -c \"\n    select site_id, primary_email_addr, username, full_name\n    from users3\n    where\n      is_admin\n      -- Exclude System and Sysbot.\n      and user_id >= 100\n    order by site_id asc, username asc\n    \")\n\necho\necho \"First, a tips:\"\necho \"To generate admin login links, go to:  https://your-talkyard-server/-/admin-login\"\necho \"and type your admin email.\"\necho\necho \"Here're the admin email addresses:\"\necho\necho \"$admin_addrs\"\necho\n\n\n# Print admin one time login links.\n\necho \"Looking in $db_user's database for admin login link emails\"\necho \"and reset password emails ...\"\n\nemails=$($dc exec rdb $psql -c \"\n    select\n       -- Remove newlines, so can count and grep properly.\n       -- (Need \\\\ not \\, because Bash eats one.)\n       regexp_replace(body_html, '[\\\\n\\\\r]+', ' ', 'g' ) || '\\n'\n    from emails_out3\n    where\n      -- This is EmailType.ResetPassword = 22 and OneTimeLoginLink = 23.\n      type in (22, 23)\n      -- One day should be enough?\n      and sent_on > now_utc() - interval '1 day'\n      order by sent_on asc\n      limit 22\n    \")\n\n\n# Sync with the email generating code [ADMLOGINEML].\nname_urls=$(echo \"$emails\" | \\\n    sed -nr 's#.*>(Hi [^<]+).*(https?://[^\"]+).*$#  \\1  \\2#p')\n\nif [ -z \"$name_urls\" ]; then\n  echo\n  echo \"Found nothing.\"\nelse\n  how_many=$(echo \"$name_urls\" | wc --lines)\n  echo\n  echo \"Found $how_many recent admin login or reset password links, most recent last:\"\n  echo\n  echo \"$name_urls\"\n  echo \"                                    ^---- this last link is the most recent\"\n  echo\n  echo \"Copy-paste the links into your browser address bar.\"\n  echo \"Each link works only once.\"\nfi\n\necho\n"
  },
  {
    "path": "scripts/impl/check-talkyard-backups.sh",
    "content": "#!/bin/bash\n\n# Will finish this script later. Placed here in impl/  for now, so it won't\n# distract others who look in scripts/.\n\n\nfunction log_message {\n  echo \"`date --iso-8601=seconds --utc` check-backups: $1\"\n}\n\nif [ $# -ne 2 ]; then\n  echo \"Usage:  $0  --send-email-if-bad  BACKUP_DIR\"\n  echo \"Or:     $0  --send-test-email\"\n  exit 1\nfi\n\nbackup_dir=\"$2\"\n\necho \"Not yet implemented.  Bye.  [BADBKPEML]\"\nexit\n\n# echo \"Checking daily backups in $2:\"\n\n\n\n# Find the most recent Postgres backup.\n# if [ older than two days — check both date in file name, and unix ctime? ]\n# then\n#   problems=\" .....\"\n# fi\n\n\n# Get the backup's random value.\n# We can just look at the textual contents of the backup, to find out if they're\n# most likely ok — no need to restore the database into a real PostgreSQL\n# server. It'd be nice to do this too, optionally, though.\n#\n# good_row=$(zcat | grep \"$random_value\" | grep \"$hostname\" | grep 'postgres.sql')\n\n# if [ -z \"$good_row\" ]\n# then\n#   problems=\" .....\"\n# fi\n\n\n# Have look in the uploads dir.\n# There should be this file:\n#    touch $backup_test_dir/$when--$(hostname)--$random_value\n# if [ no such file ]\n# then\n#   problems=\"$problems\\n ... more problems.....\"\n# fi\n\n# if [ uploads backup is older than two days ]\n# then\n#   problems=\" .....\"\n# fi\n\n\n# if [ \"$problems\" ]\n# then\n#   Send an email with the \"$problems\".  [BADBKPEML]\n#   Need SMTP server addr, username, pwd, send-to address.\n#\n#   Websearch for \"how send email from linux server\" to find out how.\n#\n# fi\n"
  },
  {
    "path": "scripts/impl/docker-compose.wrong-app-ip.yml",
    "content": "# Changes the 'app' container IP to the wrong IP [maint_app_ip],  so 'web'\n# cannot connect to it. This makes 'web' respond quickly with a status 502,\n# which is good when in maintenance mode where we want to show a\n# \"We're upgrading the server\" message — instead of 'web' being able to connect\n# to 'app'; then it won't respond to the end user until half a minute (?)\n# has elapsed.\n\nservices:\n  web:\n    extra_hosts:\n        # Try connecting to a non-existing IP on a subnet we *can* access.\n        # Because if 'web' tries to connect to something outside the subnet, seems\n        # it justs \"hang\", maybe the connection would eventually timeout?\n        #\n        # 'web' is on the fe_int_net subnet:  FE_INT_NET_SUBNET = 172.26.2.0/24\n        # and the correct app IP is:          FE_INT_NET_APP_IP = 172.26.2.41\n        # see ../.env.\n        #\n        app: 172.26.2.127  # intentionally wrong\n\n"
  },
  {
    "path": "scripts/impl/recreate-web.sh",
    "content": "#/bin/bash\n\n# This makes 'web' container config changes in docker-compose.yml take effect\n# (need to recreate the container).\n\ndocker compose kill web\ndocker compose rm -f web\ndocker compose up -d web\n"
  },
  {
    "path": "scripts/impl/unjson.sh",
    "content": "#!/bin/bash\n\n# Makes json log messages human readable, by parsing the json and pretty-printing\n# the app specific interesting fields.\n\n\nif [ -z `which jq` ]; then\n  echo \"Please install 'jq' for Json pretty print, e.g.:  sudo apt install jq\"\n  exit 1\nfi\n\n# -r preserves backslashes, otherwise '\\n' gets converted to just 'n', and we can no longer\n# pretty-print e.g. stacktraces with newlines.\nwhile read -r line\ndo\n  # Remove Docker's color codes.\n  line=$(echo \"$line\" | sed -r \"s/\\x1B\\[([0-9]{1,2}(;[0-9]{1,2})?)?[mGK]//g\")\n\n  # We'll add our own colors, here're some codes:\n  # (from http://misc.flogisoft.com/bash/tip_colors_and_formatting)\n  # \\e[34m = blue\n  # \\e[95m = light magenta\n  # \\e[32m = green\n  # \\e[33m = yellow\n  # \\e[90m = dark gray\n  # \\e[92m = light green\n  # \\e[39m = default\n\n  if [[ \"$line\" =~ ^web_ ]] ; then\n    echo -e \"`echo \"$line\" | sed -r 's/^([^|]+\\|)(.*)$/\\\\\\\\e[34m\\1\\\\\\\\e[39m\\2/'`\"\n  elif [[ \"$line\" =~ ^rdb_ ]] ; then\n    echo -e \"`echo \"$line\" | sed -r 's/^([^|]+\\|)(.*)$/\\\\\\\\e[95m\\1\\\\\\\\e[39m\\2/'`\"\n  elif [[ \"$line\" =~ ^cache_ ]] ; then\n    echo -e \"`echo \"$line\" | sed -r 's/^([^|]+\\|)(.*)$/\\\\\\\\e[32m\\1\\\\\\\\e[39m\\2/'`\"\n  elif [[ \"$line\" =~ ^search_ ]] ; then\n    echo -e \"`echo \"$line\" | sed -r 's/^([^|]+\\|)(.*)$/\\\\\\\\e[33m\\1\\\\\\\\e[39m\\2/'`\"\n  elif [[ \"$line\" =~ ^gulp_ ]] ; then\n    echo -e \"`echo \"$line\" | sed -r 's/^([^|]+\\|)(.*)$/\\\\\\\\e[90m\\1\\\\\\\\e[39m\\2/'`\"\n  elif [[ \"$line\" =~ ^app_ ]] ; then\n    # The program 'jq' extracts the timestamp, severity, message etc fields from a json log message.\n    # The -j flag removes surrounding quotes.\n    json=$( egrep -o '\\{\".*\\}' <<< \"$line\" )\n    if [ -n \"$json\" ]; then\n      pretty_json=$(echo \"$json\" | jq -j '.severity, \"  \", .message, \"  kvs: \", .kvs' )\n      app=\"$(echo \"$line\" | sed -r 's/^([^|]+\\|)(.*)$/\\1/')\"\n      echo -e \"\\e[92m$app\\e[39m $pretty_json\"\n    else\n      # In dev mode, when compiling & reloading, Play Framework logs non-json messages.\n      echo -e \"`echo \"$line\" | sed -r 's/^([^|]+\\|)(.*)$/\\\\\\\\e[92m\\1\\\\\\\\e[39m\\2/'`\"\n    fi\n  else\n    echo \"$line\"\n  fi\ndone\n\n"
  },
  {
    "path": "scripts/old/Vagrantfile",
    "content": "# -*- mode: ruby -*-\n# vi: set ft=ruby :\n\n#===========\n# What is this? This file tells a program named Vagrant how a\n# Virtual Machine (VM) can be created and configured on your computer,\n# in which you can test install Talkyard.\n#\n# ***\n# MIGHT NO LONGER WORK? Was last tested with Ubuntu 18.04, but now we're\n# at Ubuntu 22.04 — \"ubuntu/jammy64\" below. Don't know if today's Vagrant\n# is compatible with this 4 years old file?\n# ***\n#\n# Tips: You'll need to add  talkyard.port=8080  to play-framework.conf\n# (mentioned in README.md).\n#\n# Go and read about Vagrant here: https://www.vagrantup.com/ — click\n# \"Getting Started\" and read that page. Download and install Vagrant.\n# Also install VirtualBox (or some othervirtualization system), so that\n# there'll be something that can run the VM Vagrant will download for you.\n# Then:\n#\n# 1) Create an empty folder named `talkyard-prod-test`. Copy this file into it.\n# 2) cd into that folder, and type `vagrant up`\n# 3) Wait for Vagrant do download stuff, and the VM to start.\n# 4) Then type `vagrant ssh`, to open a shelll inside the VM.\n# 5) Follow the instructions in README.md (but now you have a server already,\n#    i.e. the Vagrant VM which you are inside right now).\n#    Note that, when editing /opt/talkyard/conf/play-framework.conf, you'll need to\n#    comment in:  talkyard.port=8080\n#    and set:  talkyard.secure=false\n#    since we'll access Talkyard via http://localhost:8080, no https cert.\n# 6) To stop the VM, type CTRL-D to exit Vagrant, then type `vagrant halt`.\n#\n# Advanced tips: To ssh into the VM and also expose a port on the host inside the VM,\n# do e.g.:   vagrant ssh -- -R 5000:localhost:5000\n# This is useful if you have a local test Docker registry running at localhost:5000\n# on the host, and want to make it available in the VM also at localhost:5000.\n#===========\n\n# All Vagrant configuration is done below. The \"2\" in Vagrant.configure\n# configures the configuration version (we support older styles for\n# backwards compatibility). Please don't change it unless you know what\n# you're doing.\nVagrant.configure(2) do |config|\n  # The most common configuration options are documented and commented below.\n  # For a complete reference, please see the online documentation at\n  # https://docs.vagrantup.com.\n\n  # Every Vagrant development environment requires a box. You can search for\n  # boxes at https://atlas.hashicorp.com/search.\n  config.vm.box = \"ubuntu/jammy64\"\n\n  # Disable automatic box update checking. If you disable this, then\n  # boxes will only be checked for updates when the user runs\n  # `vagrant box outdated`. This is not recommended.\n  # config.vm.box_check_update = false\n\n  # Create a forwarded port mapping which allows access to a specific port\n  # within the machine from a port on the host machine. In the example below,\n  # accessing \"localhost:8080\" will access port 80 on the guest machine.\n  config.vm.network \"forwarded_port\", guest: 80, host: 8080\n\n  # Create a private network, which allows host-only access to the machine\n  # using a specific IP.\n  # config.vm.network \"private_network\", ip: \"192.168.33.10\"\n\n  # Create a public network, which generally matched to bridged network.\n  # Bridged networks make the machine appear as another physical device on\n  # your network.\n  # config.vm.network \"public_network\"\n\n  # Share an additional folder to the guest VM. The first argument is\n  # the path on the host to the actual folder. The second argument is\n  # the path on the guest to mount the folder. And the optional third\n  # argument is a set of non-required options.\n  # config.vm.synced_folder \"../data\", \"/vagrant_data\"\n\n  # Provider-specific configuration so you can fine-tune various\n  # backing providers for Vagrant. These expose provider-specific options.\n  # Example for VirtualBox:\n  #\n  config.vm.provider \"virtualbox\" do |vb|\n    vb.customize [\"modifyvm\", :id, \"--natdnshostresolver1\", \"on\"]\n  #   # Display the VirtualBox GUI when booting the machine\n  #   vb.gui = true\n  #\n  #   # Customize the amount of memory on the VM:\n      vb.memory = \"3000\"\n  end\n  #\n  # View the documentation for the provider you are using for more\n  # information on available options.\n\n  # Define a Vagrant Push strategy for pushing to Atlas. Other push strategies\n  # such as FTP and Heroku are also available. See the documentation at\n  # https://docs.vagrantup.com/v2/push/atlas.html for more information.\n  # config.push.define \"atlas\" do |push|\n  #   push.app = \"YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME\"\n  # end\n\n  # Enable provisioning with a shell script. Additional provisioners such as\n  # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the\n  # documentation for more information about their specific syntax and use.\n  # config.vm.provision \"shell\", inline: <<-SHELL\n  #   sudo apt-get update\n  #   sudo apt-get install -y apache2\n  # SHELL\nend\n"
  },
  {
    "path": "scripts/prepare-os.sh",
    "content": "#!/bin/bash\n\n# This script makes ElasticSearch work, simplifies troubleshooting,\n# and configures automatic security updates, with reboots.\n\nfunction log_message {\n  echo \"`date --iso-8601=seconds --utc` prepare-os: $1\"\n}\n\necho\necho\nlog_message 'Configuring this Operating System:'\n\ndid_what=''\n\n# Avoid harmless \"warning: Setting locale failed\" warnings from Perl:\n# (https://askubuntu.com/questions/162391/how-do-i-fix-my-locale-issue)\nlocale-gen 'en_US.UTF-8'\nif ! grep -q 'LC_ALL=' /etc/default/locale; then\n  echo 'Setting LC_ALL to en_US.UTF-8...'\n  echo 'LC_ALL=en_US.UTF-8' >> /etc/default/locale\n  export LC_ALL='en_US.UTF-8'\n  did_what=\"Configured LC_ALL=en_US.UTF-8.\"\nfi\nif ! grep -q 'LANG=' /etc/default/locale; then\n  echo 'Setting LANG to en_US.UTF-8...'\n  echo 'LANG=en_US.UTF-8' >> /etc/default/locale\n  export LANG='en_US.UTF-8'\n  did_what=\"$did_what Configured LANG=en_US.UTF-8.\"\nfi\n\n\n# Append system config settings, so the ElasticSearch Docker container will work,\n# and so Nginx can handle more connections. [BACKLGSZ]\n\nif ! grep -q 'Talkyard' /etc/sysctl.conf; then\n  log_message 'Amending the /etc/sysctl.conf config...'\n  cat <<-EOF >> /etc/sysctl.conf\n\t\t\n\t\t###################################################################\n\t\t# Talkyard settings\n\t\t#\n\t\t# Turn off swap, default = 60.\n\t\tvm.swappiness=0\n\t\t# Up the max backlog queue size (num connections per port), default = 128.\n\t\t# Sync with conf/web/sites-enabled/talkyard-servers.conf.\n\t\tnet.core.somaxconn=8192\n\t\t# ElasticSearch wants this, default = 65530\n\t\t# See: https://www.elastic.co/guide/en/elasticsearch/reference/current/vm-max-map-count.html\n\t\tvm.max_map_count=262144\n\t\tEOF\n\n  log_message 'Reloading the system config...'\n  sysctl --system\n  did_what=\"$did_what Added Talkyard settings to /etc/sysctl.conf.\"\nelse\n  log_message 'Talkyard settings found in /etc/sysctl.conf, leaving as is.'\nfi\n\n\n# Make Redis happier:\n# Redis doesn't want Transparent Huge Pages (THP) enabled, because that creates\n# latency and memory usage issues with Redis. Disable THP now directly, and also\n# after restart: (as recommended by Redis)\nif ! grep -q '\\[always\\]' /sys/kernel/mm/transparent_hugepage/enabled ; then\n  echo \"Transparent Huge Pages is [madvise] or [never], fine, Redis happy.\"\nelse\n  echo \"Setting Transparent Huge Pages to [madvise], Redis wants this ...\"\n  echo madvise > /sys/kernel/mm/transparent_hugepage/enabled\n  # We can use rc.local — also with Systemd, see: https://askubuntu.com/a/919598.\n  rc_local_f=\"/etc/rc.local\"\n  if [ ! -f $rc_local_f ]; then\n    echo \"exit 0\" >> $rc_local_f\n  fi\n  if ! grep -q 'transparent_hugepage/enabled' $rc_local_f ; then\n    echo \"Setting Transparent Huge Pages to [madvise] after reboot, in $rc_local_f...\"\n    # Insert ('i') before the last line ('$') in rc.local, which always? is\n    # 'exit 0' in a new Ubuntu installation ... no, Debian now.\n    sed -i -e '$i # For Talkyard and the Redis Docker container:\\necho madvise > /sys/kernel/mm/transparent_hugepage/enabled\\n' $rc_local_f\n  fi\n  did_what=\"$did_what Set Transparent Huge Pages to [madvise].\"\nfi\n\n\n# Simplify troubleshooting:\nif ! grep -q 'HISTTIMEFORMAT' ~/.bashrc; then\n  log_message 'Adding history settings to .bashrc...'\n  cat <<-EOF >> ~/.bashrc\n\t\t\n\t\t###################################################################\n\t\texport HISTCONTROL=ignoredups\n\t\texport HISTCONTROL=ignoreboth\n\t\texport HISTSIZE=10100\n\t\texport HISTFILESIZE=10100\n\t\texport HISTTIMEFORMAT='%F %T %z  '\n\t\tEOF\n  did_what=\"$did_what Added HIST* settings to .bashrc.\"\nelse\n  log_message 'Probably sensible settings found in .bashrc, leaving as is.'\nfi\n\n\n# [ty_v1] Auto upgr:  Ask if, and recommend that, auto reboot if needed after security\n# upgrades, and if Yes, then, add Automatic-Reboot also if there's already\n# a 20auto-upgrades file.  Seems such a file exists by default, nowadays, Debian 12.\n\n# Automatically apply OS security patches.\n# The --force-confdef/old tells Apt to not overwrite any existing configuration, and to ask no questions.\n# See e.g.: https://askubuntu.com/a/104912/48382.\n# APT::Periodic::AutoremoveInterval \"14\"; = remove auto-installed dependencies that are no longer needed.\n# APT::Periodic::AutocleanInterval \"14\";  = remove downloaded installation archives that are nowadays out-of-date.\n# APT::Periodic::MinAge \"8\" = packages won't be deleted until they're these many days old (default is 2).\n# more docs: less /usr/lib/apt/apt.systemd.daily\nauto_upgr_f=\"/etc/apt/apt.conf.d/20auto-upgrades\"\nif [ -f $auto_upgr_f ]; then\n  log_message \"There's already an auto upgrades config file: $auto_upgr_f.\"\n  log_message \"I'll leave it as is — I won't (re)configure automatic upgrades.\"\n  log_message \"---- It's contents: ------\"\n  cat $auto_upgr_f\n  log_message \"--------------------------\"\n  log_message \"Consider adding the below line,  if it's missing,\"\n  log_message \"so your server will reboot if needed, for upgrades to take effect:\"\n  echo\n  echo 'Unattended-Upgrade::Automatic-Reboot \"true\";'\n  echo\nelse\n  log_message 'Enabling automatic security updates and reboots...'\n  did_what=\"$did_what Enabled automatic security updates and reboots.\"\n  # About the packages we install:\n  # apt-config-auto-update: Makes APT automatically update its package cache.\n  # unattended-upgrades: Downloads and installs security upgrades automatically and unattended.\n  DEBIAN_FRONTEND=noninteractive \\\n      apt-get install -y \\\n          -o Dpkg::Options::=\"--force-confdef\" \\\n          -o Dpkg::Options::=\"--force-confold\" \\\n          unattended-upgrades \\\n          apt-config-auto-update\n  cat <<EOF > $auto_upgr_f\nAPT::Periodic::Update-Package-Lists \"always\";\nAPT::Periodic::Unattended-Upgrade \"always\";\nAPT::Periodic::AutoremoveInterval \"14\";\nAPT::Periodic::AutocleanInterval \"14\";\nAPT::Periodic::MinAge \"8\";\nUnattended-Upgrade::Automatic-Reboot \"true\";\nEOF\nfi\n\n\nlog_message \"Done configuring the OS.\"\n\nif [ -z \"$did_what\" ]; then\n  log_message \"I did nothing — everything seemed ok already.\"\nelse\n  log_message \"I did this: $did_what\"\nfi\necho\n\n# vim: ts=2 sw=2 tw=0 fo=r list\n"
  },
  {
    "path": "scripts/schedule-automatic-upgrades.sh",
    "content": "#!/bin/bash\n\nfunction log_message {\n  echo \"`date --iso-8601=seconds --utc` schedule-upgrades: $1\"\n}\n\necho\nlog_message \"Scheduling automatic upgrades...\"\n\nupgrade_match=$(crontab -l | grep '/opt/talkyard-v1 .*/upgrade-if-needed.sh')\n\n# We backup at 02:10, delete old backups at 03:10 (see schedule-daily-backups.sh),\n# so let's check for new versions and upgrade, at 04:10.\n\nif [ -z \"$upgrade_match\" ]; then\n\tcrontab -l | { cat; echo '10 4 * * * cd /opt/talkyard-v1 && ./scripts/upgrade-if-needed.sh >> talkyard-maint.log 2>&1'; } | crontab -\n\tlog_message \"Added entry to crontab. Done. Bye.\"\nelse\n\tlog_message \"Already done. Nothing to do. Bye.\"\nfi\necho\n\n"
  },
  {
    "path": "scripts/schedule-daily-backups.sh",
    "content": "#!/bin/bash\n\nfunction log_message {\n  echo \"`date --iso-8601=seconds --utc` schedule-backups: $1\"\n}\n\n\necho\nlog_message \"Scheduling automatic backups...\"\ndid_something=''\n\nbackup_match=$(crontab -l | grep '/opt/talkyard-v1 .*/backup.sh ')\ndelete_match=$(crontab -l | grep '/opt/talkyard-v1 .*/delete-old-backups.sh')\n\n\nif [ -z \"$backup_match\" ]; then\n\tcrontab -l | { cat; echo '10 2 * * * cd /opt/talkyard-v1 && ./scripts/backup.sh daily >> talkyard-maint.log 2>&1'; } | crontab -\n\tlog_message \"Added backup.sh to crontab.\"\n\tdid_something='yes'\nfi\n\nif [ -z \"$delete_match\" ]; then\n\tcrontab -l | { cat; echo '10 3 * * * cd /opt/talkyard-v1 && ./scripts/delete-old-backups.sh >> talkyard-maint.log 2>&1'; } | crontab -\n\tlog_message \"Added delete-old-backups.sh to crontab.\"\n\tdid_something='yes'\nfi\n\nif [ -n \"$did_something\" ]; then\n\tlog_message \"Done. Bye.\"\nelse\n\tlog_message \"Already done. Nothing to do. Bye.\"\nfi\necho\n\n"
  },
  {
    "path": "scripts/tests/install-all.sh",
    "content": "#!/bin/bash\n\n# This is just for testing, for now, so don't actually run this script, unless:\nif [ \"$1\" != \"really\" ]; then\n  echo \"No? Really? Yes? Not really? Yes but not in reality?\"\n  exit 1\nfi\n\necho \"Ok, let's test install all.\"\n\n# This runs all installation scripts, and, NOT IMPL:\n#    sets the hosthame to $1, sets random passwords and starts Ty.\n\n# Need the repo first:\n# ----\n#apt-get update\n#apt-get -y install git vim locales\n#apt-get -y install tree ncdu                # nice to have\n#locale-gen en_US.UTF-8                      # installs English\n#export LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8  # starts using English (warnings are harmless)\n#\n#cd /opt/\n#git clone https://github.com/debiki/talkyard-prod-one.git talkyard\n#cd talkyard\n# ----\n\n./scripts/prepare-os.sh 2>&1 | tee -a talkyard-maint.log\n\n./scripts/install-docker-compose.sh 2>&1 | tee -a talkyard-maint.log\n\n./scripts/start-firewall.sh 2>&1 | tee -a talkyard-maint.log\n\n\nvi conf/play-framework.conf  # fill in values in the Required Settings section\n# or:\nsed --in-place=.orig 's/=\"change_this\"/=\"changeeee_thisss_ok_done\"/'  conf/play-framework.conf\n\n# and:\necho 'test-db-pw' > secrets/postgres_password.txt\n\ncp mem/2g.yml docker-compose.override.yml\n\n\n./scripts/upgrade-if-needed.sh 2>&1 | tee -a talkyard-maint.log\n\n./scripts/schedule-logrotate.sh 2>&1 | tee -a talkyard-maint.log\n./scripts/schedule-daily-backups.sh 2>&1 | tee -a talkyard-maint.log\n./scripts/schedule-automatic-upgrades.sh 2>&1 | tee -a talkyard-maint.log\n\n"
  },
  {
    "path": "scripts/tests/install-docker-compose.sh",
    "content": "#!/bin/bash\n\n# This installs Docker and Docker-Compose on a totally new & blank Debian server,\n# based on: https://docs.docker.com/engine/install/debian/\n\nfunction log_message {\n  echo \"`date --iso-8601=seconds --utc` install-docker: $1\"\n}\n\necho\nlog_message \"Installing Docker and Docker-Compose...\"\n\n\n# ------- Add Docker repository\n\n# But what about Debian 12 and 13?\n\n# Move to README.md?\n# Install packages to allow apt to use a repository over HTTPS:\napt-get update\napt-get -y install \\\n    apt-transport-https \\\n    ca-certificates \\\n    curl \\\n    gnupg \\\n    software-properties-common\n\n# Add Docker’s official GPG key.\n# And check its sha256 hash — in case the Docker servers has been compromised?\n# (Note that the Debian packages are later downloaded from the same server,\n# that is, download.docker.com, so, an attacker might be able to modify\n# both the packages, and the keyring file, at the same time?\n# Indeed, someone else has commented about this, and suggests that the\n# public key be available in other ways than only via docker.com:\n# https://github.com/docker/for-linux/issues/849#issuecomment-554721114 )\nd_gpg_f=\"/etc/apt/keyrings/docker.gpg\"\nif [ -f $d_gpg_f ]; then\n  log_message \"Docker GPG key already present: $d_gpg_f, fine.\"\nelse\n  gpg_url=\"https://download.docker.com/linux/debian/gpg\"\n  log_message \"Downloading Docker GPG key to: $d_gpg_f, from: $gpg_url ...\"\n  sudo mkdir -p /etc/apt/keyrings\n  curl -fsSL \"$gpg_url\" | gpg --dearmor -o $d_gpg_f\n  sudo chmod a+r $d_gpg_f\n\n  # As of 2021-03-19 ... and 2022-10-20 ... and 2023-07-10  [hash_instead]\n  # Works for both Debian and Ubuntu (apparently same gpg key).\n  gpg_hash_expected=\"a09e26b72228e330d55bf134b8eaca57365ef44bf70b8e27c5f55ea87a8b05e2\"\n  gpg_hash_actual=\"$(sha256sum $d_gpg_f)\"\n  if [[ ! $gpg_hash_actual =~ $gpg_hash_expected ]]; then\n    echo\n    log_message \"Unexpected SHA256 hash of: $d_gpg_f\"\n    log_message \"Expected: $gpg_hash_expected\"\n    log_message \"But sha256sum says:\"\n    log_message \"  $gpg_hash_actual\"\n    log_message \"Is something amiss? I don't know. Aborting installation.\"\n    echo\n    log_message \"ERROR, see above.\"\n    exit 1\n  fi\n  log_message \"Done. Docker GPG key SHA256 hash looks fine.\"\nfi\n\n\n# Check that the fingerprint is correct:\n# But how do that, using only gpg? not apt-key?  [hash_instead]\n# (see https://docs.docker.com/engine/installation/linux/debian/#install-using-the-repository)\n# Sth like:\n# pub_key_expected='9DC858229FC7DD38854AE2D88D81803C0EBFCD88'\n# if [[ ! $(gpg $d_gpg_f) =~ $pub_key_expected ]]; then\n# echo\n# log_message \"ERROR: Bad Docker GPG key fingerprint. [TyEDKRFNGR]\"\n# log_message \"Don't continue installing.\"\n# log_message \"Instead, ask for help in the Docker forums: https://forums.docker.com/,\"\n# log_message \"and show them the output from running this:\"\n# log_message \"    apt-key fingerprint 0EBFCD88\"\n# log_message \"and include a link to this script too, here it is:\"\n# log_message \"    https://github.com/debiki/talkyard-prod-one/blob/master/scripts/install-docker-compose.sh\"\n# echo\n# exit 1\n#fi\n\nd_list_f=\"/etc/apt/sources.list.d/docker.list\"\nif [ -f $d_list_f ]; then\n  log_message \"Docker Apt repo config file already here: $d_list_f, fine.\"\nelse\n  log_message \"Adding Docker Apt repo in $d_list_f:\"\n  echo \\\n      \"deb [arch=\"$(dpkg --print-architecture)\" signed-by=$d_gpg_f] \\\nhttps://download.docker.com/linux/debian \\\n\"$(. /etc/os-release && echo \"$VERSION_CODENAME\")\" stable\" \\\n      | tee $d_list_f\nfi\n\n\n# ------- Install Docker CE:\n\n\n# List versions: apt-cache madison docker-ce\n# Upgrade:\n#   service docker stop\n#   apt-get update\n#   apt-get upgrade  # hmm seems to upgrade Docker too, also if installed via docker-ce=...\n#   apt-get -y install docker-ce=VERSION   # or is this needed?\n\n# To use a specific version:  (don't forget the '=', the first character)\n#EQ_DOCKER_VERSION=\"=1.5-2\"\n# But the Debian default version is probably ok, so just skip '=VERSION':\nEQ_DOCKER_VERSION=\"\"\n\nif [ ! -z \"$(which docker)\" ]; then\n  log_message  \"Docker already installed, fine.\"\nelse\n  log_message \"Installing Docker $EQ_DOCKER_VERSION...\"\n  apt-get update\n  apt-get -y install \\\n        docker-ce$EQ_DOCKER_VERSION \\\n        docker-ce-cli$EQ_DOCKER_VERSION \\\n        containerd.io \\\n        docker-buildx-plugin \\\n        docker-compose-plugin\nfi\n\nlog_message \"Testing Docker: Running 'docker run hello-world' ...\"\n\nHELLO_WORLD=\"$(docker run hello-world | grep -i 'hello ')\"\nif [ -z \"$HELLO_WORLD\" ]; then\n  echo\n  log_message \"Error installing or starting Docker: 'docker run hello-world' doesn't work. [EdEDKRBROKEN]\"\n  log_message \"Ask for help in the Talkyard forum: https://www.talkyard.io/forum/\"\n  log_message \"and/or in the Docker forums: https://forums.docker.com/\"\n  echo\n  log_message \"ERROR, see above.\"\n  exit 1\nfi\n\necho\nlog_message \"The Docker hello-world image says:  $HELLO_WORLD\"\necho\nlog_message \"Docker worked fine. Installing Docker-Compose ...\"\n\n\nservice docker start\n\n# Make everything start automatically on server startup. Not needed though:\n# on Debian and Ubuntu, the Docker service is configured to start on boot by default.\n# And if the server admins have changed that, leave as is.\n#systemctl enable docker.service\n#systemctl enable containerd.service\n\n\n# Enable log rotation.\n\nd_conf_f=\"/etc/docker/daemon.json\"\nif [ -f \"$d_conf_f\" ]; then\n  log_message \"There's already a Docker daemon.json: $d_conf_f,\"\n  log_message \"I'll leave it as is; I won't configure Docker log rotation.\"\nelse\n  log_message \"Creating $d_conf_f with Docker log rotation settings...\"\n  echo '\n{\n  \"log-driver\": \"json-file\",\n  \"log-opts\": {\n    \"max-size\": \"25m\",\n    \"max-file\": \"5\"\n  }\n}\n' | tee -a $d_conf_f\n  systemctl restart docker\nfi\n\n\n\n# Install Docker Compose (see https://github.com/docker/compose/releases)\n\napt-get install -y docker-compose-plugin\n\nlog_message\nlog_message\nlog_message \"*** Done ***\"\nlog_message\nlog_message \"Docker and Docker-Compose installed.\"\nlog_message\nlog_message \"This should print 'Docker Compose version v2.40' or later:\"\nlog_message \"----------------------------\"\ndocker compose version\nd_c_status_code=\"$?\"\nlog_message \"----------------------------\"\necho\n\nif [ $d_c_status_code -ne 0 ]; then\n  log_message \"ERROR: docker compose didn't work, see above. Bye.\"\n  exit 1\nfi\n\nexit 0\n"
  },
  {
    "path": "scripts/tests/test-delete-backups.sh",
    "content": "#!/usr/bin/env bash\n\nif [ \"$1\" != \"danger\" ]; then\n  echo \"\"\n  echo \"You didn't say danger\"\n  echo \"\"\n  echo \"Don't run this script. It's maybe dangerous.\"\n  echo \"\"\n  exit 1\nfi\n\nexport ORIG_PATH=\"$PATH\"\nexport PATH=\"/usr/bin:/bin\"  # that's what Cron sees, see:\n# https://stackoverflow.com/questions/2135478/how-to-simulate-the-environment-cron-executes-a-script-with\n\nexport ORIG_DATE=$(date)\ndate --set \"2018-08-30 03:30:00\"   # sync with test-generate-backups.sh [4ABKR207]\n\n./scripts/delete-old-backups.sh\n\ndate --set \"$ORIG_DATE\"   # minus the time taken, to delete backups  :-(\nexport PATH=\"$ORIG_PATH\"\n\n"
  },
  {
    "path": "scripts/tests/test-generate-backups.sh",
    "content": "#!/usr/bin/env bash\n\nif [ \"$1\" != \"danger\" ]; then\n  echo \"\"\n  echo \"You didn't say danger\"\n  echo \"\"\n  echo \"Don't run this script. It messes up your computer's date-time.\"\n  echo \"\"\n  echo \"Usage:  $0  danger  [scripts_dir_prefix]\"\n  echo \"\"\n  echo \"where  scripts_dir_prefix  is an optional path to where ./scripts/backup.sh is.\"\n  echo\n  echo \"Example:\"\n  echo \"  ./scripts/tests/test-generate-backups.sh  danger\"\n  echo \"  ./modules/ed-prod-one-test/scripts/tests/test-generate-backups.sh  danger  ./modules/ed-prod-one-test/\"\n  echo\n  exit 1\nfi\n\n# See `echo ...` above.\nscripts_dir_prefix=\"${2:-.}\"\n\nexport ORIG_PATH=\"$PATH\"\nexport PATH=\"/usr/bin:/bin\"  # that's how Cron works, see:\n# https://stackoverflow.com/questions/2135478/how-to-simulate-the-environment-cron-executes-a-script-with\n\nexport ORIG_DATE=$(date)\n\necho \"If you want to restore the original date, minus time elapsed, run:\"\necho\necho \"date --set \\\"$ORIG_DATE\\\"\"\n\n\n\nfunction backup_at {\n  date_time_colon=\"$1\"  # e.g. \"2030-08-15 21:30:11\"\n\n  echo >> talkyard-maint.log\n  echo \"Setting new date: $date_time_colon\" >> talkyard-maint.log\n  echo >> talkyard-maint.log\n\n  #date_time_t=$(echo \"$date_time_colon\" | sed 's/://g' | sed 's/ /T/')\n  date --set \"$date_time_colon\"\n  # touch $backup_archives_dir/dummy-hostname-2018-06-01T0210Z-daily-postgres.sql.gz\n  $scripts_dir_prefix/scripts/backup.sh autotest 2>&1 | tee -a talkyard-maint.log\n}\n\n\nfunction delete_old_backups_at {\n  date_time_colon=\"$1\"\n\n  echo >> talkyard-maint.log\n  echo \"Setting new date: $date_time_colon\" >> talkyard-maint.log\n  echo >> talkyard-maint.log\n\n  date --set \"$date_time_colon\"\n  $scripts_dir_prefix/scripts/delete-old-backups.sh 2>&1 | tee -a talkyard-maint.log\n}\n\n\ndelete_old_backups_at \"2022-01-01 00:00:01\"\nbackup_at \"2022-01-01 10:00:00\"\nbackup_at \"2022-01-02 10:00:00\"\nbackup_at \"2022-01-03 10:00:00\"\necho\necho \"Took 3 backups: 2022-01-01 to -01-03, would you like to check?\"\nread -p \"Press enter to continue\"\necho\n\ndelete_old_backups_at \"2022-01-03 11:00:01\"\necho\necho \"That should have deleted nothing\"\nread -p \"Press enter to continue\"\necho\n\nbackup_at \"2022-01-04 10:00:00\"\nbackup_at \"2022-01-05 10:00:00\"\nbackup_at \"2022-01-06 10:00:00\"\nbackup_at \"2022-01-07 10:00:00\"\nbackup_at \"2022-01-08 10:00:00\"\nbackup_at \"2022-01-09 10:00:00\"\nbackup_at \"2022-01-10 10:00:00\"\nbackup_at \"2022-01-11 10:00:00\"\nbackup_at \"2022-01-12 10:00:00\"\nbackup_at \"2022-01-13 10:00:00\"\nbackup_at \"2022-01-14 10:00:00\"\nbackup_at \"2022-01-15 10:00:00\"\nbackup_at \"2022-01-16 10:00:00\"\nbackup_at \"2022-01-17 10:00:00\"\nbackup_at \"2022-01-18 10:00:00\"\nbackup_at \"2022-01-19 10:00:00\"\nbackup_at \"2022-01-20 10:00:00\"\necho\necho \"Took 17 backups, 20 in total.\"\nread -p \"Press enter to continue\"\necho\ndelete_old_backups_at \"2022-01-16 11:00:01\"\necho\necho \"That should have deleted ? Postgres, ? Config and some Redis backups.\"\necho \"See  min_recent_bkps=8  and  'run_find -mtime +14'  in  scripts/delete-old-backups.sh\"\necho\nread -p \"Press enter to continue\"\necho\n\nbackup_at \"2022-02-01 10:00:00\"\nbackup_at \"2022-03-01 10:00:00\"\nbackup_at \"2022-04-01 10:00:00\"\nbackup_at \"2022-05-01 10:00:00\"\nbackup_at \"2022-06-01 10:00:00\"\necho\necho \"Took 5 montly backups.\"\nread -p \"Press enter to continue\"\necho\ndelete_old_backups_at \"2022-01-09 11:00:01\"\necho\necho \"That should have deleted one Uploads backup.\"\necho \"See  recent_bkps=  find  -not -mtime +123  in scripts/delete-old-backups.sh\"\necho\necho \"And some Redis backups.\"\necho \"But no Postgres or Config backups — min_recent_bkps=8.\"\necho\nread -p \"Press enter to continue\"\necho\n\nbackup_at \"2022-06-02 10:00:00\"\n\nbackup_at \"2022-07-01 10:00:00\"\nbackup_at \"2022-07-02 10:00:00\"\nbackup_at \"2022-07-03 10:00:00\"\nbackup_at \"2022-07-04 10:00:00\"\nbackup_at \"2022-07-05 10:00:00\"\nbackup_at \"2022-07-06 10:00:00\"\nbackup_at \"2022-07-07 10:00:00\"\nbackup_at \"2022-07-08 10:00:00\"\nbackup_at \"2022-07-09 10:00:00\"\nbackup_at \"2022-07-10 10:00:00\"\nbackup_at \"2022-07-11 10:00:00\"\nbackup_at \"2022-07-12 10:00:00\"\nbackup_at \"2022-07-13 10:00:00\"\nbackup_at \"2022-07-14 10:00:00\"\nbackup_at \"2022-07-15 10:00:00\"\nbackup_at \"2022-07-16 10:00:00\"\nbackup_at \"2022-07-17 10:00:00\"\nbackup_at \"2022-07-18 10:00:00\"\nbackup_at \"2022-07-19 10:00:00\"\nbackup_at \"2022-07-20 10:00:00\"\nbackup_at \"2022-07-21 10:00:00\"\nbackup_at \"2022-07-22 10:00:00\"\nbackup_at \"2022-07-23 10:00:00\"\nbackup_at \"2022-07-24 10:00:00\"\nbackup_at \"2022-07-25 10:00:00\"\nbackup_at \"2022-07-26 10:00:00\"\nbackup_at \"2022-07-27 10:00:00\"\nbackup_at \"2022-07-28 10:00:00\"\nbackup_at \"2022-07-29 10:00:00\"\nbackup_at \"2022-07-30 10:00:00\"\nbackup_at \"2022-07-31 10:00:00\"\n\nbackup_at \"2022-08-01 10:00:00\"\nbackup_at \"2022-08-02 10:00:00\"\nbackup_at \"2022-08-03 10:00:00\"\nbackup_at \"2022-08-04 10:00:00\"\nbackup_at \"2022-08-05 10:00:00\"\nbackup_at \"2022-08-06 10:00:00\"\nbackup_at \"2022-08-07 10:00:00\"\nbackup_at \"2022-08-08 10:00:00\"\nbackup_at \"2022-08-09 10:00:00\"\nbackup_at \"2022-08-10 10:00:00\"\nbackup_at \"2022-08-11 10:00:00\"\nbackup_at \"2022-08-12 10:00:00\"\nbackup_at \"2022-08-13 10:00:00\"\nbackup_at \"2022-08-14 10:00:00\"\nbackup_at \"2022-08-15 10:00:00\"\nbackup_at \"2022-08-16 10:00:00\"\nbackup_at \"2022-08-17 10:00:00\"\nbackup_at \"2022-08-18 10:00:00\"\nbackup_at \"2022-08-19 10:00:00\"\nbackup_at \"2022-08-20 10:00:00\"\nbackup_at \"2022-08-21 10:00:00\"\nbackup_at \"2022-08-22 10:00:00\"\nbackup_at \"2022-08-23 10:00:00\"\nbackup_at \"2022-08-24 10:00:00\"\nbackup_at \"2022-08-25 10:00:00\"\nbackup_at \"2022-08-26 10:00:00\"\nbackup_at \"2022-08-27 10:00:00\"\nbackup_at \"2022-08-28 10:00:00\"\nbackup_at \"2022-08-29 10:00:00\"\nbackup_at \"2022-08-30 10:00:00\"\nbackup_at \"2022-08-31 10:00:00\"\n\nbackup_at \"2022-09-01 10:00:00\"\nbackup_at \"2022-09-02 10:00:00\"\nbackup_at \"2022-09-03 10:00:00\"\nbackup_at \"2022-09-04 10:00:00\"\nbackup_at \"2022-09-05 10:00:00\"\nbackup_at \"2022-09-06 10:00:00\"\nbackup_at \"2022-09-07 10:00:00\"\nbackup_at \"2022-09-08 10:00:00\"\nbackup_at \"2022-09-09 10:00:00\"\nbackup_at \"2022-09-10 10:00:00\"\nbackup_at \"2022-09-11 10:00:00\"\nbackup_at \"2022-09-12 10:00:00\"\nbackup_at \"2022-09-13 10:00:00\"\nbackup_at \"2022-09-14 10:00:00\"\nbackup_at \"2022-09-15 10:00:00\"\nbackup_at \"2022-09-16 10:00:00\"\nbackup_at \"2022-09-17 10:00:00\"\nbackup_at \"2022-09-18 10:00:00\"\nbackup_at \"2022-09-19 10:00:00\"\nbackup_at \"2022-09-20 10:00:00\"\nbackup_at \"2022-09-21 10:00:00\"\nbackup_at \"2022-09-22 10:00:00\"\nbackup_at \"2022-09-23 10:00:00\"\nbackup_at \"2022-09-24 10:00:00\"\nbackup_at \"2022-09-25 10:00:00\"\nbackup_at \"2022-09-26 10:00:00\"\nbackup_at \"2022-09-27 10:00:00\"\nbackup_at \"2022-09-28 10:00:00\"\nbackup_at \"2022-09-29 10:00:00\"\nbackup_at \"2022-09-30 10:00:00\"\n\n\nbackup_at \"2022-10-01 10:00:00\"\nbackup_at \"2022-10-02 10:00:00\"\nbackup_at \"2022-10-03 10:00:00\"\nbackup_at \"2022-10-04 10:00:00\"\nbackup_at \"2022-10-05 10:00:00\"\nbackup_at \"2022-10-06 10:00:00\"\nbackup_at \"2022-10-07 10:00:00\"\nbackup_at \"2022-10-08 10:00:00\"\nbackup_at \"2022-10-09 10:00:00\"\nbackup_at \"2022-10-10 10:00:00\"\nbackup_at \"2022-10-11 10:00:00\"\nbackup_at \"2022-10-12 10:00:00\"\nbackup_at \"2022-10-13 10:00:00\"\nbackup_at \"2022-10-14 10:00:00\"\nbackup_at \"2022-10-15 10:00:00\"\nbackup_at \"2022-10-16 10:00:00\"\nbackup_at \"2022-10-17 10:00:00\"\nbackup_at \"2022-10-18 10:00:00\"\nbackup_at \"2022-10-19 10:00:00\"\nbackup_at \"2022-10-20 10:00:00\"\nbackup_at \"2022-10-21 10:00:00\"\nbackup_at \"2022-10-22 10:00:00\"\nbackup_at \"2022-10-23 10:00:00\"\nbackup_at \"2022-10-24 10:00:00\"\nbackup_at \"2022-10-25 10:00:00\"\nbackup_at \"2022-10-26 10:00:00\"\nbackup_at \"2022-10-27 10:00:00\"\nbackup_at \"2022-10-28 10:00:00\"\nbackup_at \"2022-10-29 10:00:00\"\nbackup_at \"2022-10-30 10:00:00\"\nbackup_at \"2022-10-31 10:00:00\"\necho\necho \"What now?\"\necho\n\n\necho\necho\necho \"If you want to restore the original date, minus time elapsed: $ORIG_DATE\"\necho\necho \"date --set \\\"$ORIG_DATE\\\"\"\necho\n\nexport PATH=\"$ORIG_PATH\"\n\n"
  },
  {
    "path": "scripts/upgrade-if-needed.sh",
    "content": "#!/bin/bash\n\n# Exit on any error.\nset -e\n\nlog_message() {\n  echo \"`date --iso-8601=seconds --utc` upgrade-script: $1\"\n}\n\ncheck_single_line() {\n  # This: `\\n` instead of `$\\n` would look for '\\' and 'n', two chars, instead of a newline.\n  if [[ $1 =~ $'\\n' ]]; then\n    log_message \"Error: $2 is multiple lines: '$1'\"\n    exit 1\n  fi\n}\n\n# $1: Version nr. $2: Nr from where.\ncheck_version_is_epoch_1() {\n  if ! [[ $1 =~ ^v1\\. ]]; then\n    log_message \"ERROR: Bad version nr in $2, not epoch 1: '$1'. Bye. [TyEUPEPOCHNR]\" >&2\n    exit 1\n  fi\n}\n\ndocker='/usr/bin/docker'\ndocker_compose=\"$docker compose\"\n\necho\n\n\n# Determine release branch\n# ===========================\n\nRELEASE_BRANCH_LINE=\"$(grep -E '^ *RELEASE_BRANCH=.*$' .env)\"\nRELEASE_BRANCH=\"$(sed -nr 's/^RELEASE_BRANCH= *([^# ]+) *$/\\1/p' .env)\"\nif [ -z \"$RELEASE_BRANCH\" ]; then\n  if [ -n \"$RELEASE_BRANCH_LINE\" ]; then\n    log_message \"ERROR: Weird RELEASE_BRANCH=... line in .env: (between ---)\"\n    log_message \"----\"\n    log_message \"$RELEASE_BRANCH_LINE\"\n    log_message \"----\"\n  else\n    # There's this line by default:  RELEASE_BRANCH=tyse-v1-regular — did they delete it?\n    log_message \"ERROR: No RELEASE_BRANCH=... specified in .env.\"\n  fi\n  exit 1\nelse\n  log_message \"Using release branch: $RELEASE_BRANCH.\"\nfi\n\n# This script (and others in this repo) are compatible only with Talkyard epoch 1.\n# (-e is for the pattern, needed since starts w '-'. It's not --extended-regexp, that's -E.)\nif [ -z \"$(echo \"$RELEASE_BRANCH\" | grep -e '-v1-')\" ]; then\n  log_message \"ERROR: Wrong epoch in release branch. Should be '...-v1-...'\"\n  log_message \"but is: '$RELEASE_BRANCH'.\"\n  exit 1\nfi\n\n# No ambiguities please.\ncheck_single_line \"$RELEASE_BRANCH_LINE\"  'RELEASE_BRANCH=...'\n\n\n\n# Determine current version\n# ===========================\n\nCURRENT_VERSION=\"$(sed -nr 's/^ *VERSION_TAG=([a-zA-Z0-9\\._-]*).*/\\1/p' .env)\"\nif [ -z \"$CURRENT_VERSION\" ]; then\n  log_message \"Apparently no Talkyard v1 version currently installed.\"\n  log_message \"Checking for latest version...\"\nelse\n  check_single_line        \"$CURRENT_VERSION\"  'VERSION_TAG=... in .env'\n  check_version_is_epoch_1 \"$CURRENT_VERSION\"  'VERSION_TAG=... in .env'\n  log_message \"Current version: $CURRENT_VERSION\"\n  log_message \"Checking for newer versions...\"\nfi\n\n\n# Determine new version\n# ===========================\n\n# We'll find the next Talkyard version, by pulling a version list from a Git repo\n# and looking at the last line in a version list file.\n# (The version number changes a bit unpredictably, so we can't just bump it. And it\n# also includes the Git revision which is \"random\".)\n\nif [ ! -f versions/version-tags.log ]; then\n  log_message \"Downloading version numbers submodule...\"\n  /usr/bin/git submodule update --init\nfi\n\ncd versions\n/usr/bin/git fetch origin\n# This creates a branch named $RELEASE_BRANCH if it didn't already exist.\n# Then checks out that branch, and hard-resets it to origin/$RELEASE_BRANCH.\n# And sets it to track that origin branch (which isn't really needed since we\n# hard-reset here anyway).\n/usr/bin/git checkout -B \"$RELEASE_BRANCH\" --track \"origin/$RELEASE_BRANCH\"\ncd ..\n\nNEXT_VERSION=\"$(tail -n1 versions/version-tags.log)\"\n\nif [ -z \"$NEXT_VERSION\" ]; then\n  log_message \"ERROR: Can't find any Talkyard version in versions/version-tags.log.\"\n  log_message \"Don't know what to do. Bye. [EdEUPNOVER]\"\n  exit 1\nfi\ncheck_single_line        \"$NEXT_VERSION\"  '`tail -n1 versions/version-tags.log`'\ncheck_version_is_epoch_1 \"$NEXT_VERSION\"  '`tail -n1 versions/version-tags.log`'\n\nPINNED_VERSION=\"$(sed -nr 's/^ *PINNED_VERSION_TAG=([a-zA-Z0-9\\._-]*).*/\\1/p' .env)\"\nif [ -n \"$PINNED_VERSION\" ]; then\n  check_single_line        \"$PINNED_VERSION\"  'PINNED_VERSION_TAG=... in .env'\n  check_version_is_epoch_1 \"$PINNED_VERSION\"  'PINNED_VERSION_TAG=... in .env'\n  log_message \"Pinned version: $PINNED_VERSION\"\n\n  if [[ \"$CURRENT_VERSION\" == \"$PINNED_VERSION\" ]]; then\n    log_message \"Pinned version is same as current version. Need do nothing. Bye.\"\n    echo\n    exit 0\n  fi\n\n  log_message \"Setting next version to pinned version.\"\n  NEXT_VERSION=\"$PINNED_VERSION\"\nfi\n\n\n# Decide what to do\n# ===========================\n\nif [ \"$CURRENT_VERSION\" == \"$NEXT_VERSION\" ]; then\n  log_message \"No new version to upgrade to. Doing nothing. Bye.\"\n  echo\n  exit 0\nfi\n\nif [ -z \"$CURRENT_VERSION\" ]; then\n  log_message \"I will install version $NEXT_VERSION.\"\n  WHAT='Installing'\nelse\n  log_message \"I will upgrade to $NEXT_VERSION.\"\n  log_message \"Backing up before upgrading...\"\n  ./scripts/backup.sh \"$CURRENT_VERSION\"\n  echo \"$CURRENT_VERSION\" >> previous-version-tags.log\n  WHAT='Upgrading'\nfi\n\n\n# Remove old Talkyard images & containers\n# ===========================\n\n# So won't run out of disk. Let's keep images less than a year old, in case\n# need to downgrade to previous server version:  31 * 12 * 24h = 8928.\n# Also, do this whilst the old containers are still running, so their images\n# won't be removed (that is, before the Upgrade step below).\n#\n# But skip, if on a pinned version — don't know how old it is, and maybe it's\n# not running right now.\n\nif [[ -n \"$CURRENT_VERSION\" && -z \"$PINNED_VERSION\" ]]; then\n  # Let's make this work both with reverse DNS key names, and without (just \"talkyard\").\n  # And if moving from .io to .app / .dev / .org TLD in the future, hmm.\n  log_message \"Removing any unused Talkyard images older than a year ...\"\n  for label in \"io.talkyard\" \"app.talkyard\" \"dev.talkyard\" \"org.talkyard\" \"talkyard\" ; do\n    # --all removes also unused but not-dangling images, but not volumes\n    # (need to add --volumes to remove volumes too).\n    $docker system prune --all --force --filter \"until=8928h\" \\\n            --filter \"label=$label.edition=tyse\" \\\n            --filter \"label=$label.epoch=1\"\n  done\nfi\n\n\n# Download new version\n# ===========================\n\n# `docker-compose.yml` uses the environment variable `$PINNED_VERSION_TAG` in the image tags,\n# for example, the app service:\n#    image: ${DOCKER_REG_ORG}/talkyard-app:${PINNED_VERSION_TAG:-${VERSION_TAG}}\n# So, by setting PINNED_VERSION_TAG we can make Docker download the next version.\nlog_message \"Downloading version $NEXT_VERSION... (this might take long)\"\nPINNED_VERSION_TAG=\"$NEXT_VERSION\" $docker_compose pull\n\n\n# Upgrade\n# ===========================\n\n\n# Shut down old version\n# ```````````````````````````\n\nif [ -n \"$CURRENT_VERSION\" ]; then\n  log_message \"Upgrading: Shutting down old version $CURRENT_VERSION...\"\n  # Stop 'app' before 'web', otherwise Play Framework (in 'app') logs warnings\n  # about \"ConnectionClosed PeerClosed\". Better stop 'search' first of all, in case\n  # ElasticSearch is a bit slow with reacting — so 'app' continues handling requests,\n  # meanwhile.\n  $docker_compose stop search\n  $docker_compose stop app\n\n  # And, in case ty-main was left running if this upgrade script was aborted in the middle:\n  # (If it's still running, `down` below won't be able to recreate the networks, and\n  # ty-maint might have occupied an IP addr we need.)\n  set +e\n  $docker kill ty-maint\n  set -e\n\n  $docker_compose down\n  log_message \"Upgrading: Done shutting down.\"\nfi\n\n\n# Start any database migration\n# ```````````````````````````\n\nlog_message \"$WHAT: Starting v$NEXT_VERSION, the app and databases ...\"\nPINNED_VERSION_TAG=\"$NEXT_VERSION\" $docker_compose up -d app\n\n\n# Under Maintenance message\n# ```````````````````````````\n\nif [ -n \"$CURRENT_VERSION\" ]; then\n  log_message \"$WHAT: Starting 'web': Showing an Under Maintenance page\"\n\n  # For whatever reason (although we use `run --rm` below) the ty-maint container\n  # might already exist. Then, remove it. But if it doesn't, then, disable `set -e`\n  # so this script won't exit here when `rm` fails.\n  set +e\n  $docker rm -f ty-maint\n  set -e\n\n  # Start 'web' and change the 502.html error page to an Under Maintenance page.\n  #\n  # Also change 'app's IP addr so 'web' cannot connect to it  [maint_app_ip]\n  # — otherwise, if 'web' can connect to Play Framework in 'app', then, making\n  # requests to 'web' hangs, waiting for 'app' to have started completely.\n  # We cannot use `--add-host=app:172.26.0...` — that param is for `docker`\n  # only not `docker compose`. Instead, we add docker-compose.wrong-app-ip.yml\n  # which does the same thing.\n  #\n  # Also, need to explicitly mount the Nginx config volumes, otherwise, when using\n  # 'run', apparently they're not mounted.\n  #\n  set +e  # if doesn't work, harmless\n  PINNED_VERSION_TAG=\"$NEXT_VERSION\"  \\\n      $docker_compose \\\n                        -f docker-compose.yml  \\\n                        -f scripts/impl/docker-compose.wrong-app-ip.yml  \\\n        run --rm -d --no-deps  \\\n          --name ty-maint  \\\n          -p80:80  -p443:443  \\\n          -e TY_MAINT_MODE=true  \\\n          -v ./conf/web/maint-msg.html:/opt/nginx/html/502.html  \\\n          web\n  set -e\n\n  # Poll-wait until: the app server has started, and is done with any database\n  # migration, and with warming up the Nashorn Javascript engine.\n  # We need to run cURL in the 'app' container, because 'web' is temporarily\n  # connected to the wrong IP. [maint_app_ip]\n  log_message \"$WHAT: Waiting for the app server to have started ...\"\n  # (We've done: `set -e`, but that ignores `if` and `until` tests.)\n  # Specify --noproxy so curl won't try to use egressp. [egressp_conf]\n  until $docker exec -i \"$($docker_compose ps -q app)\"  \\\n            curl --output /dev/null --silent --head --fail --noproxy '*' \\\n                 http://localhost:9000/-/are-scripts-ready\n  do\n    printf '.'\n    sleep 1\n  done\n\n  log_message \"$WHAT: App server has started. Removing the Under Maintenance message ...\"\n  set +e\n  $docker stop ty-maint\n  set -e\nfi\n\n# Start everything\n# ```````````````````````````\n\n# Just 'web' left to start.\nlog_message \"$WHAT: Starting 'web' (Nginx) ...\"\nPINNED_VERSION_TAG=\"$NEXT_VERSION\" $docker_compose up -d\n\n\n# Done. Bump version\n# ===========================\n\n# Bump the current version number, but not until after 'docker compose up' above\n# has exited successfully so we know it works.\nlog_message \"$WHAT: Setting current version number to $NEXT_VERSION...\"\nsed --in-place=.prev-version -r \"s/^(VERSION_TAG=)([a-zA-Z0-9\\\\._-]*)(.*)$/\\1$NEXT_VERSION\\3/\" .env\n\n\nlog_message \"Done. Bye.\"\necho\n\n# vim: et ts=2 sw=2 tw=0 fo=r\n"
  },
  {
    "path": "view-logs",
    "content": "#!/bin/bash\n\ndocker compose logs $@ | scripts/impl/unjson.sh\n"
  },
  {
    "path": "view-stats",
    "content": "#!/bin/bash\n\ndocker stats `docker ps --format '{{.Names}}'`\n\n"
  }
]