Installation and deployment behind nginx
From a bare Ubuntu box to a working HTTPS site: building, PostgreSQL, external configuration, systemd, nginx, and the default settings you have to change.
These instructions assume Ubuntu 22.04/24.04 or Debian 12 and a single machine hosting the application, the database and nginx. On a different distribution only the package names change.
Внимание
The application.properties shipped in the repository is a development
configuration. It contains a file path from someone else's workstation, a
plaintext administrator password and port 80, which collides with nginx. The
«What you must change» section is not a set of
suggestions — it is a precondition for the thing working at all.
The end state#
1. Dependencies#
sudo apt update && sudo apt install -y openjdk-21-jdk-headless postgresql nginx git
Spring Boot 4 requires Java 17 as a minimum; pom.xml declares
<java.version>17</java.version>, but you can build and run on any newer LTS —
Java 21 is preferable because of its garbage collector. Verify that the version
was actually picked up:
java -version
2. The database#
sudo -u postgres psql -c "CREATE USER otryad WITH PASSWORD 'REPLACE_ME';"
sudo -u postgres psql -c "CREATE DATABASE otryadwebsite OWNER otryad;"
There is no need to create the schema by hand: the project sets
spring.jpa.hibernate.ddl-auto=update, and Hibernate builds the tables itself
on first startup.
Осторожно
ddl-auto=update can add columns and tables, but it never drops or
narrows existing ones. After an upgrade that renames fields, the old columns
stay in the database — harmless, but the schema needs manual attention. Run
pg_dump before every upgrade.
3. Building#
Note that the Maven project lives in a nested directory, OtryadWebsite/,
rather than at the repository root. Build from there.
git clone https://github.com/Electronprod/OtryadWebsite.git /tmp/otryad-src
cd /tmp/otryad-src/OtryadWebsite && ./mvnw -B clean package -DskipTests
The result is target/OtryadWebsite-1.0.jar, an executable fat jar. Lombok and
DevTools do not end up inside it: the former is excluded in the
spring-boot-maven-plugin configuration, the latter is marked optional and
Spring Boot does not package it into the assembled archive.
Put everything in place:
sudo useradd --system --home /var/lib/otryadwebsite --shell /usr/sbin/nologin otryad
sudo mkdir -p /opt/otryadwebsite /var/lib/otryadwebsite/media /etc/otryadwebsite
sudo cp target/OtryadWebsite-1.0.jar /opt/otryadwebsite/app.jar
sudo chown -R otryad:otryad /var/lib/otryadwebsite
4. What you must change#
There is no need to touch application.properties inside the jar — Spring Boot
picks up an external file and it overrides the bundled one. Create
/etc/otryadwebsite/application.properties:
# --- port: 80 is taken by nginx ---
server.port=8080
server.address=127.0.0.1
# --- behind a reverse proxy: otherwise the host in sitemap and JSON-LD is wrong ---
server.forward-headers-strategy=native
# --- files ---
system.storage.path=/var/lib/otryadwebsite/media/
web.storage.url-prefix=/media/
web.storage.local-serving=false
# --- upload sizes: keep in sync with client_max_body_size in nginx ---
spring.servlet.multipart.max-file-size=2GB
spring.servlet.multipart.max-request-size=2GB
# --- database ---
spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/otryadwebsite
spring.datasource.username=otryad
spring.datasource.password=REPLACE_ME
# --- first administrator: used only when no admin exists in the database yet ---
security.admin.login=REPLACE_ME
security.admin.password=REPLACE_ME8
# --- turn off the development switch ---
dev=false
What matters here and why:
| Parameter | In the repository | Why it has to change |
|---|---|---|
server.port | ${SERVER_PORT:80} | 80 is taken by nginx — the application will not start |
system.storage.path | a Windows path under a user's Downloads folder | a path from the developer's workstation |
web.storage.local-serving | true | at true files are served by Spring rather than nginx, and the whole point of the proxy is lost |
server.forward-headers-strategy | commented out | without it the application believes it is reachable at http://127.0.0.1:8080, and that address ends up in sitemap.xml, og:url and JSON-LD |
security.admin.password | a default value committed to the repository | the default administrator password is public |
spring.servlet.multipart.max-file-size | 10GB | together with the nginx limit this sets the real maximum; 10 GB in a single request is almost always a mistake |
dev | true | a development flag — it has no business being on in production |
Важно
The administrator password must pass validation on the Person entity: at
least 8 characters, at least one Latin letter and at least one digit.
Otherwise the application starts but fails to create the first admin and dies
with a validation error. The security.admin.* pair is read only when the
users table contains no user with the ROLE_ADMIN role — changing it
after the first startup is pointless; passwords are changed through the panel.
5. systemd#
A critical detail: site settings are stored in a properties.json file, and the
path to it in the code is relative — the file is created in the process's
current working directory. That makes WorkingDirectory mandatory: without it
the file lands in / or in whatever directory the service happened to be
started from, and after a migration all settings, SEO entries and short links
appear to "vanish".
/etc/systemd/system/otryadwebsite.service:
[Unit]
Description=OtryadWebsite
After=network-online.target postgresql.service
Wants=network-online.target
[Service]
User=otryad
Group=otryad
WorkingDirectory=/var/lib/otryadwebsite
ExecStart=/usr/bin/java -Xmx512m -jar /opt/otryadwebsite/app.jar \
--spring.config.additional-location=file:/etc/otryadwebsite/
Restart=on-failure
RestartSec=5
SuccessExitStatus=143
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
ReadWritePaths=/var/lib/otryadwebsite
[Install]
WantedBy=multi-user.target
Permissions on the file holding the passwords, then start it:
sudo chmod 640 /etc/otryadwebsite/application.properties && sudo chown root:otryad /etc/otryadwebsite/application.properties
sudo systemctl daemon-reload && sudo systemctl enable --now otryadwebsite
Check that it came up:
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/
Logs are at journalctl -u otryadwebsite -f. On the first startup they should
contain Administrator profile not found, creating a new one....
6. nginx#
/etc/nginx/sites-available/otryadwebsite:
server {
listen 80;
server_name example.org www.example.org;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name example.org www.example.org;
ssl_certificate /etc/letsencrypt/live/example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.org/privkey.pem;
# must be no smaller than spring.servlet.multipart.max-file-size
client_max_body_size 2G;
# the app compresses only html/xml/json — css, js and svg are left to nginx
gzip on;
gzip_types text/css application/javascript image/svg+xml application/json;
gzip_min_length 1024;
# media files: served from disk, Java untouched
location /media/ {
alias /var/lib/otryadwebsite/media/;
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
try_files $uri =404;
}
# telemetry does not need to face the internet, even though it requires auth
location /actuator/ { deny all; }
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
# uploading large videos from the panel
proxy_request_buffering off;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}
Enable it and verify the configuration:
sudo ln -s /etc/nginx/sites-available/otryadwebsite /etc/nginx/sites-enabled/ && sudo nginx -t && sudo systemctl reload nginx
The certificate:
sudo apt install -y certbot python3-certbot-nginx && sudo certbot --nginx -d example.org -d www.example.org
Why it is set up this way#
alias, not root. Public file addresses are built as
web.storage.url-prefix + batchId + file name, that is
/media/<uuid>/01.jpg. On disk that is <system.storage.path>/<uuid>/01.jpg.
alias discards the /media/ prefix; root would keep it and you would end up
with /var/lib/otryadwebsite/media/media/<uuid>/01.jpg. The trailing slashes on
location and alias are mandatory — both of them, at the same time.
Uploads still go through Java. nginx serves media for reading only.
POST /dashboard/media/upload is handled by Spring, so the size limit is needed
both in nginx (client_max_body_size) and in the application
(spring.servlet.multipart.*). The smaller of the two is the real limit; nginx
returns 413 when it is exceeded.
X-Forwarded-* is useless without forward-headers-strategy. The
application builds absolute addresses through ServletUriComponentsBuilder, and
those end up in sitemap.xml, in Open Graph and in JSON-LD. Without native
they will carry the internal address and the http scheme, and search engines
will index links that lead nowhere.
7. After installation#
- Go to
https://example.org/authand log in with thesecurity.admin.*pair. Immediately change the password under/dashboard/usersand create separate accounts for the moderators. - Turn on protection mode in
/dashboardwhile you are filling the site with content: the public part closes to everyone except staff and holders of the token link. - Replace the domain in
robots.txt— it ships with the original deployment's domain hardcoded in theSitemap:directive. The file lives insrc/main/resources/static/, so editing it means editing the sources and rebuilding. The alternative, without a rebuild, is to intercept/robots.txtwith its ownlocationblock in nginx. - Walk through the titles and descriptions in
/dashboard/seo: the shipped defaults describe the original organization. - Check
https://example.org/sitemap.xml— the addresses inside must start with your domain andhttps. If they do not,server.forward-headers-strategydid not take effect.
8. Backups#
The state of the site consists of three independent things, and losing any one of them loses content:
#!/bin/sh
# /usr/local/bin/otryad-backup.sh
set -eu
DST=/var/backups/otryad/$(date +%F)
mkdir -p "$DST"
sudo -u postgres pg_dump otryadwebsite | gzip > "$DST/db.sql.gz"
cp /var/lib/otryadwebsite/properties.json "$DST/"
tar czf "$DST/media.tar.gz" -C /var/lib/otryadwebsite media
properties.json is small, but nothing can reconstruct it: it holds the SEO
data for every page, the short links, the contacts, the social links and the
access token.
9. Upgrading#
cd /tmp/otryad-src && git pull && cd OtryadWebsite && ./mvnw -B clean package -DskipTests
sudo -u postgres pg_dump otryadwebsite | gzip > /var/backups/otryad-pre-upgrade.sql.gz
sudo systemctl stop otryadwebsite
sudo cp target/OtryadWebsite-1.0.jar /opt/otryadwebsite/app.jar
sudo systemctl start otryadwebsite
A few seconds of downtime are unavoidable: there is a single instance and sessions are held in memory, so anyone logged into the panel is signed out by the restart.
Common problems#
| Symptom | Cause |
|---|---|
The service dies on startup with Address already in use in the logs | server.port was not overridden and the application is trying to claim 80 |
| All site settings reset after a restart | WorkingDirectory is not set — properties.json was created in a different directory |
| Album images fail to load with 404 | system.storage.path and alias disagree, or one of them lost its trailing slash |
| The album opens but is empty | the batchId directory holds nothing but preview.jpg, which is always excluded from the gallery |
sitemap.xml contains addresses like http://127.0.0.1:8080/... | server.forward-headers-strategy=native did not take effect |
| A large video upload fails with 413 | client_max_body_size is smaller than the file |
| A large upload fails with 500 | the nginx limit was raised but spring.servlet.multipart.max-file-size was not |
| Logging into the panel returns 429 with JSON | the attempt limit kicked in: 5 per minute per address, 5 per minute per login, 30 per day per address |
/dashboard returns 403 after logging in | the user's role is neither ROLE_ADMIN nor ROLE_MODERATOR |