Skip to content

Installing with systemd (RHEL family)

This page walks through building engawa from scratch on AlmaLinux 10, Rocky Linux 10 or RHEL 10. Version 9 and earlier are not supported.

If you are on Ubuntu or Debian, see Installing with systemd (Ubuntu / Debian).

Item Guideline
Server 4GB of RAM or more recommended. The build step is memory-hungry
Domain e.g. example.com. Point an A record (or AAAA for IPv6) at the server first
Mail server Not required, but you need one to send registration confirmation mails
Object storage Not required. You can configure it later from the admin panel
  1. Install the required software (Node.js, PostgreSQL, Valkey)
  2. Create a dedicated user and a database
  3. Fetch the source and build it
  4. Write the configuration file
  5. Register it as a systemd service
  6. Put a reverse proxy (nginx) in front and serve HTTPS, including SELinux and firewalld

Run the commands below as a regular user with sudo access.

Bring the system up to date and install the build tools.

ターミナルウィンドウ
sudo dnf upgrade -y
sudo dnf group install -y "Development Tools"
sudo dnf install -y python3 curl git

Development Tools and python3 are needed to compile some of engawa’s dependencies. Without them, pnpm install fails later on.

Add the EPEL repository now as well, since it is needed later. EPEL packages can depend on the CRB repository, so enable CRB first.

On AlmaLinux and Rocky Linux:

ターミナルウィンドウ
sudo dnf install -y dnf-plugins-core
sudo dnf config-manager --set-enabled crb
sudo dnf install -y epel-release

dnf config-manager comes from dnf-plugins-core, which minimal installs may lack, so install it first.

On RHEL itself (epel-release is not in its standard repositories, so install the RPM that EPEL publishes):

ターミナルウィンドウ
sudo subscription-manager repos --enable codeready-builder-for-rhel-10-$(arch)-rpms
sudo dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm

engawa shells out to the ffmpeg and ffprobe commands to handle video. Neither ships in the standard RHEL-family repositories, so you have to add them.

The easy route is ffmpeg-free from EPEL:

ターミナルウィンドウ
sudo dnf install -y ffmpeg-free

If you need the full build with patent-encumbered codecs, add RPM Fusion:

ターミナルウィンドウ
sudo dnf install -y https://mirrors.rpmfusion.org/free/el/rpmfusion-free-release-10.noarch.rpm
sudo dnf install -y ffmpeg

Either way the commands are called ffmpeg and ffprobe. Confirm they are there:

ターミナルウィンドウ
ffprobe -version

engawa needs Node.js 24 or newer. The default repositories ship Node.js 24 as nodejs24, but it installs the command as node-24 rather than node, which does not fit the rest of this guide. Add the NodeSource repository instead.

ターミナルウィンドウ
curl -fsSL https://rpm.nodesource.com/setup_24.x | sudo bash -
sudo dnf install -y nodejs

Check it:

ターミナルウィンドウ
node -v

A version starting with v24. means you are good.

Next, enable pnpm. engawa uses pnpm, not npm or yarn.

ターミナルウィンドウ
sudo corepack enable
ターミナルウィンドウ
pnpm -v

engawa needs PostgreSQL 15 or newer. To get a recent version (18 here), this guide installs it from PostgreSQL’s official Yum repository.

ターミナルウィンドウ
sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-10-$(uname -m)/pgdg-redhat-repo-latest.noarch.rpm
ターミナルウィンドウ
sudo dnf install -y postgresql18-server postgresql18-contrib

Unlike Ubuntu, the RHEL family does not initialize the data directory for you. The official packages use a per-version setup command:

ターミナルウィンドウ
sudo /usr/pgsql-18/bin/postgresql-18-setup initdb
sudo systemctl enable postgresql-18
sudo systemctl start postgresql-18

psql and the other commands are also linked into /usr/bin. This page writes full paths under /usr/pgsql-18/bin/ anyway, so the matching version is used even if several are installed.

Out of the box, local connections use ident — no password — which means engawa cannot log in. Change it:

ターミナルウィンドウ
sudo nano /var/lib/pgsql/18/data/pg_hba.conf

Near the bottom, set the last column of the 127.0.0.1/32 and ::1/128 lines to scram-sha-256:

# IPv4 local connections:
host all all 127.0.0.1/32 scram-sha-256
# IPv6 local connections:
host all all ::1/128 scram-sha-256

Save, then reload the configuration:

ターミナルウィンドウ
sudo systemctl reload postgresql-18

The standard RHEL 10 repositories do not carry Redis; they ship Valkey, a compatible fork, instead. engawa connects to it just as it would to Redis.

ターミナルウィンドウ
sudo dnf install -y valkey
sudo systemctl enable --now valkey

Verify it is running:

ターミナルウィンドウ
valkey-cli ping

It should answer PONG.

Running engawa as root is best avoided, so create a dedicated user.

ターミナルウィンドウ
sudo useradd -m -s /bin/bash engawa

From here on, anything touching engawa itself is done as this user. Switch to it with:

ターミナルウィンドウ
sudo -iu engawa

Type exit to return to your own user. Each step below says which user to run it as.

Go back to your own user (the one with sudo) and run:

ターミナルウィンドウ
sudo -u postgres /usr/pgsql-18/bin/psql

At the postgres=# prompt, run these three lines. Replace example-pass with a password of your own and keep it — you will put it in the configuration file.

CREATE ROLE engawa WITH LOGIN PASSWORD 'example-pass';
CREATE DATABASE engawa OWNER engawa;
\q

\q leaves the prompt. It is worth confirming that the new credentials actually work:

ターミナルウィンドウ
/usr/pgsql-18/bin/psql -h 127.0.0.1 -U engawa -d engawa -c '\conninfo'

If it prompts for the password and connects, the authentication change in step 3 took effect.

From here on, work as the engawa user.

ターミナルウィンドウ
sudo -iu engawa

Clone the repository:

ターミナルウィンドウ
git clone --branch develop https://github.com/bettaku/engawa.git engawa
cd engawa

Install the dependencies. This takes a few minutes.

ターミナルウィンドウ
pnpm install --frozen-lockfile

Copy the sample and edit it, still as the engawa user.

ターミナルウィンドウ
cp .config/example.yml .config/default.yml
nano .config/default.yml

At a minimum, adjust these:

# The public URL. The trailing slash is required
url: https://example.com/
# The port engawa listens on. nginx forwards to it
port: 3000
db:
host: localhost
port: 5432
db: engawa
user: engawa
pass: example-pass # the password from step 6
redis:
host: localhost
port: 6379
# Trust the reverse proxy you set up in step 11.
# Without this, every request is recorded as coming from the proxy's IP
trustProxy:
- '127.0.0.1/32'
- '::1/128'
# Do not change this after the server has been created
id: 'aidx'

In nano, save with Ctrl+O → Enter and quit with Ctrl+X.

Every other option is documented with comments in the sample file, so it is worth reading through once. For how the file relates to environment variables, see Backend environment variables; for fine-tuning the database connection, see Options for db.extra.

Still as the engawa user, from the repository root:

ターミナルウィンドウ
NODE_ENV=production pnpm build

Once the build finishes, create the tables:

ターミナルウィンドウ
pnpm migrate

Then start it by hand once to check everything works:

ターミナルウィンドウ
pnpm start

A line like Now listening on port 3000 means it worked. Stop it with Ctrl+C and move on.

If it errors out, re-check the database password and the Valkey settings.

Type exit to leave the engawa user and return to your own.

Create the service file:

ターミナルウィンドウ
sudo nano /etc/systemd/system/engawa.service

Paste this in and save:

[Unit]
Description=engawa daemon
After=network-online.target postgresql-18.service valkey.service
[Service]
Type=simple
User=engawa
Group=engawa
UMask=0027
WorkingDirectory=/home/engawa/engawa
ExecStart=/usr/bin/node /home/engawa/engawa/packages/backend/built/boot/entry.js
Environment="NODE_ENV=production"
TimeoutSec=60
StandardOutput=journal
StandardError=journal
SyslogIdentifier=engawa
Restart=always
[Install]
WantedBy=multi-user.target

Load and start it:

ターミナルウィンドウ
sudo systemctl daemon-reload
sudo systemctl enable --now engawa

Check the status:

ターミナルウィンドウ
sudo systemctl status engawa

active (running) is what you want. To follow the logs:

ターミナルウィンドウ
sudo journalctl -u engawa -f

Leave with Ctrl+C.

11. Put a reverse proxy in front and serve HTTPS

Section titled “11. Put a reverse proxy in front and serve HTTPS”

engawa is running on localhost:3000, which nothing outside the server can reach. Put nginx in front of it to handle HTTPS and certificates.

The repository ships production-ready proxy configuration in contrib/, and that is what this guide uses. It is worth reading contrib/README.md once as well.

The ACME module needs nginx 1.29.0 or newer, which AppStream does not carry. Add the official nginx repository.

ターミナルウィンドウ
sudo tee /etc/yum.repos.d/nginx.repo > /dev/null <<'EOF'
[nginx-mainline]
name=nginx mainline repo
baseurl=http://nginx.org/packages/mainline/centos/$releasever/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true
EOF

If AppStream’s nginx is already installed, remove it first. If it is not, dnf just says there is no match, and you can move on.

ターミナルウィンドウ
sudo dnf remove -y nginx-core

Now install from the official repository:

ターミナルウィンドウ
sudo dnf install -y nginx nginx-module-acme

Check the version:

ターミナルウィンドウ
nginx -v

nginx/1.29.0 or newer is what you need.

Open the main configuration file:

ターミナルウィンドウ
sudo nano /etc/nginx/nginx.conf

Near the top, outside the http { block, add this one line:

load_module modules/ngx_http_acme_module.so;

Copy contrib/nginx.sample.conf from the repository into nginx’s conf.d:

ターミナルウィンドウ
sudo cp /home/engawa/engawa/contrib/nginx.sample.conf /etc/nginx/conf.d/engawa.conf

Replace the domain and the contact address with your own:

ターミナルウィンドウ
sudo sed -i 's/engawa\.example\.com/example.com/g; s/admin@example\.com/you@example.com/g' /etc/nginx/conf.d/engawa.conf

Open the file, confirm the substitutions landed, and check these against your environment:

ターミナルウィンドウ
sudo nano /etc/nginx/conf.d/engawa.conf
Setting Default What it is
resolver 1.1.1.1 1.0.0.1 The DNS servers the ACME module uses. Point it at your own resolver if you have one
ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt Change it to /etc/pki/tls/certs/ca-bundle.crt, as above
client_max_body_size 256m The upload limit. Keep it in line with engawa’s maxFileSize

The official nginx package ships a default site that also listens on port 80. Remove it so it does not compete:

ターミナルウィンドウ
sudo rm -f /etc/nginx/conf.d/default.conf

The certificate and the account key are stored here, and the nginx worker user has to be able to read and write it.

ターミナルウィンドウ
sudo install -d -m 700 -o nginx -g nginx /var/lib/nginx/acme-engawa
sudo restorecon -R /var/lib/nginx/acme-engawa

restorecon resets the SELinux labels on the directory to the correct values.

Out of the box, SELinux forbids nginx from connecting to another process — engawa, in this case. Allow it:

ターミナルウィンドウ
sudo setsebool -P httpd_can_network_connect 1
ターミナルウィンドウ
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload

Port 80 is not just for the redirect to HTTPS — the HTTP-01 challenge uses it for the initial issuance and every renewal. Leave it open.

Check the syntax, then start nginx:

ターミナルウィンドウ
sudo nginx -t
sudo systemctl enable --now nginx

Only start once you see syntax is ok and test is successful. If nginx is already running, use sudo systemctl reload nginx.

The first issuance takes a few tens of seconds, and HTTPS does not work until it completes. Watch the progress and any errors in the nginx log:

ターミナルウィンドウ
sudo tail -f /var/log/nginx/error.log
ターミナルウィンドウ
curl -I http://example.com/

HTTP/1.1 308 Permanent Redirect means the redirect from HTTP works.

ターミナルウィンドウ
curl -I https://example.com/

HTTP/2 200 means the certificate was issued and requests reach engawa.

Open https://example.com in a browser. If the initial setup screen appears, you are done — the first account you create becomes the administrator.

Alternative to step 11: serving HTTPS with Caddy

Section titled “Alternative to step 11: serving HTTPS with Caddy”

This is the Caddy route instead of nginx. If you completed step 11, skip this section. Both want ports 80 and 443, so you cannot run them side by side.

Caddy obtains and renews certificates and redirects HTTP to HTTPS out of the box, so there is no extra module to install.

The standard RHEL-family repositories do not carry Caddy, so install it from COPR.

ターミナルウィンドウ
sudo dnf install -y 'dnf-command(copr)'
sudo dnf copr enable -y @caddy/caddy
sudo dnf install -y caddy

Check the version:

ターミナルウィンドウ
caddy version

Something starting with v2. means you are good.

ターミナルウィンドウ
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --permanent --add-port=443/udp
sudo firewall-cmd --reload

Port 80 is used for issuance and renewal (the HTTP-01 challenge), so leave it open. UDP 443 is for HTTP/3; skip it if you do not want HTTP/3.

Unlike the nginx route, Caddy does not normally need the httpd_can_network_connect boolean: its binary is not labelled as a web server the way nginx’s is, so it runs without those restrictions.

If you do hit a 502, check whether SELinux is involved:

ターミナルウィンドウ
sudo ausearch -m AVC -ts recent

Copy contrib/Caddyfile from the repository:

ターミナルウィンドウ
sudo cp /home/engawa/engawa/contrib/Caddyfile /etc/caddy/Caddyfile

Replace the domain and the contact address with your own. The contact address is where Let’s Encrypt sends certificate notices.

ターミナルウィンドウ
sudo sed -i 's/engawa\.example\.com/example.com/g; s/admin@example\.com/you@example.com/g' /etc/caddy/Caddyfile

Open it and confirm the substitutions landed:

ターミナルウィンドウ
sudo nano /etc/caddy/Caddyfile

max_size under request_body (268435456, i.e. 256 MiB) is meant to line up with engawa’s maxFileSize. If you change one, change the other.

Caddy can format and validate the file. Running both before you start it saves time.

ターミナルウィンドウ
caddy fmt --diff /etc/caddy/Caddyfile

A diff here means the formatting is off — it does not mean the config is broken.

ターミナルウィンドウ
sudo caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile

Valid configuration is what you want.

ターミナルウィンドウ
sudo systemctl enable --now caddy

If Caddy is already running, use sudo systemctl reload caddy.

The first issuance takes a few tens of seconds. Watch the progress and any errors in the log:

ターミナルウィンドウ
sudo journalctl -u caddy -f
ターミナルウィンドウ
curl -I http://example.com/

HTTP/1.1 308 Permanent Redirect means the redirect from HTTP works — Caddy sets that up for you.

ターミナルウィンドウ
curl -I https://example.com/

HTTP/2 200 means the certificate was issued and requests reach engawa.

Open https://example.com in a browser. If the initial setup screen appears, you are done — the first account you create becomes the administrator.

When a new version is released, run this as the engawa user:

ターミナルウィンドウ
sudo systemctl stop engawa
sudo -iu engawa
cd engawa
git pull
pnpm install --frozen-lockfile
NODE_ENV=production pnpm build
pnpm migrate
exit
sudo systemctl start engawa

If the release notes describe extra steps, follow those instead.

Symptom What to check
502 Bad Gateway First confirm you ran sudo setsebool -P httpd_can_network_connect 1. It is the most common cause here
Nothing reaches the server at all Check that http/https are open in firewalld with sudo firewall-cmd --list-services
HTTPS does not work / no certificate is issued Check that ssl_trusted_certificate was changed to /etc/pki/tls/certs/ca-bundle.crt and that port 80 is reachable from outside. Details land in /var/log/nginx/error.log
nginx will not start: module ... is already loaded The load_module line is duplicated between nginx.conf and modules-enabled/. Keep only one
Every request appears to come from the same IP trustProxy is missing from the configuration file
The service keeps dying Read the error with sudo journalctl -u engawa -n 50. A typo in the config file is the usual cause
Cannot connect to the database Check that /var/lib/pgsql/18/data/pg_hba.conf says scram-sha-256 and that you ran sudo systemctl reload postgresql-18
The timeline does not update by itself Check that the nginx WebSocket headers (Upgrade and Connection) are present
The build stops half way Out of memory. Add swap as described in step 9
Something is off and you are running Caddy Read the log with sudo journalctl -u caddy -n 50
No video thumbnails Check that ffmpeg is installed with ffprobe -version. Restart engawa after installing it

To tell whether SELinux is the culprit, look at the recent denials:

ターミナルウィンドウ
sudo ausearch -m AVC -ts recent

Whenever you edit the configuration file, restart with sudo systemctl restart engawa — a running process will not pick up the change.