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