Roadiz CMS 2.7.54 on AWS User Guide
Overview
This guide covers launching and using the Roadiz CMS 2.7.54 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg CMS, Blogs and Wikis listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
Roadiz is a free and open source headless CMS built on Symfony. Content lives in a tree of nodes whose types (pages, menus, blocks) are defined in code, editors work in the Rozier back-office at /rz-admin, and websites and apps read the content through a JSON-LD/JSON content API under /api (with OpenAPI documentation at /api/docs). It has documents and folders with image resizing, tags, translations, custom forms, user groups and roles, a revision history and webhooks.
The cloudimg image adds:
- No shared database, no baked secret. The image ships no database at all (the MariaDB and Redis data directories are empty), no configuration file, no JWT key and no compiled cache. Your instance's first boot initialises its own database, generates every secret Roadiz needs (the Symfony kernel secret that signs CSRF, "remember me" and login links, the JWT key pair and its passphrase, the database password, a health check token and the administrator password) and installs Roadiz with Roadiz's own command line before the web server starts. Roadiz has no browser installer, so there is no setup page for anyone to find.
- Secrets never on a command line. The administrator password is handed to Roadiz on standard input and hashed by Roadiz's own user code; the JWT passphrase travels in a file only root can read. Nothing from the image build is reused.
- The newest release, from an audited package set. Roadiz 2.7.54 (released 29 October 2026) through its official distribution
roadiz/skeleton2.7.9, installed with Composer without development packages. The 188 PHP packages are checked against the exact list that was audited for known vulnerabilities before the build (composer auditreports none), and Composer itself is not left on the image. - HTTPS from the first boot. nginx serves Roadiz on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS, and the instance's other addresses (its IP addresses) redirect to the one host name Roadiz is configured with.
- Fail closed. nginx and the background worker do not start until first boot has finished; if first boot cannot complete, the site stays offline rather than open.
- Only Roadiz's front controller runs PHP. Every request goes to
public/index.php; any other.phpaddress, theindex.php/...form, dotfiles such as.env.local, and everything outside thepublicfolder are never served. The folders PHP writes uploaded documents (/files/) and resized images (/assets/) to are static only: every file getsnosniffand a sandboxing content security policy, and only passive types (images, PDF, fonts, audio and video) open in the browser; anything else, SVG and HTML included, is sent as a download. - Rate-limited sign in. On top of Symfony's own login throttling (three failed sign ins per user and address per minute), nginx allows 10 back-office sign-in, login link, password reset, API token and public form submissions a minute from one address (after a burst of 10), 300 content API requests a minute and 600 PHP requests a minute, using the address of the connection, never a header a client sends.
- Links from other sites cannot act for you. The one back-office link that downloads a remote image and creates a document (
/rz-admin/documents/random) only works when it comes from your site itself (Step 9). - The real host and client address. nginx hands Roadiz the host name you configure and nothing a request claims, Symfony refuses any other host name (
SYMFONY_TRUSTED_HOSTS), and the command line builds links from the same name. nginx removesX-Forwarded-For,Client-IPand the other forwarding headers, and Roadiz trusts no proxy, so its log records the address of the connection. - No calls to third-party services until you configure them. The skeleton's placeholder DeepL key (which made every dashboard view call api.deepl.com) and Gravatar pictures (a browser call to gravatar.com with the hash of each user's e-mail) are switched off, and no mail is sent until you set a mail server (Step 12).
- Hardened defaults. The Roadiz code belongs to root and is read only to PHP,
.env.localis readable by PHP but never writable, and onlyvar/,public/filesandpublic/assetsare writable. PHP cannot run shell commands and is confined to the site folder. MariaDB has no network listener at all, and Redis (the cache, sessions and job queue) listens only on a local socket that only Redis and the web server's user can open. nginx, PHP, MariaDB, Redis, the background worker and the first boot service are blocked from the instance metadata service, the AMI requires IMDSv2, and requests for a host name the instance does not serve are dropped. - Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: Roadiz 2.7.54 (roadiz/skeleton 2.7.9), MariaDB 10.11, Redis 7.0, PHP 8.3 FPM, nginx 1.24, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.
Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large
Prerequisites
- An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet (or a private route you use for management).
- A security group that allows TCP 22 (SSH), TCP 443 (Roadiz) and TCP 80 (redirects to HTTPS) from the networks that should reach them. Keep 22 to your own networks. Open 443 to your editors and to the servers or apps that read the content API.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Roadiz CMS 2.7.54 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large, your key pair and a security group as above.
With the AWS CLI (replace the values in angle brackets):
aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
--instance-type m5.large --key-name <your-key-pair> \
--security-group-ids <your-security-group> --subnet-id <your-subnet> \
--metadata-options HttpTokens=required,HttpEndpoint=enabled \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=roadiz}]'
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| Roadiz CMS 2.7.54 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
First boot takes about a minute (it creates the database, generates the secrets and the JWT key pair and installs Roadiz). Then nginx, PHP, MariaDB, Redis, the background worker and the first boot unit are active:
systemctl is-active nginx php8.3-fpm mariadb redis-server roadiz-messenger roadiz-firstboot
active
active
active
active
active
active
nginx serves ports 80 and 443; PHP, MariaDB and Redis listen on local sockets only, so the only open ports are 22, 80 and 443:
sudo ss -tlnp | awk 'NR==1 || /nginx|sshd|php|maria|redis/' | awk '{print $1, $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g'
State Local Peer
LISTEN 0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
LISTEN 0.0.0.0:22 users:(("sshd"),("systemd"))
LISTEN 0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
LISTEN [::]:443 users:(("nginx"),("nginx"),("nginx"))
LISTEN [::]:22 users:(("sshd"),("systemd"))
LISTEN [::]:80 users:(("nginx"),("nginx"),("nginx"))
The Roadiz release and the PHP, MariaDB, Redis and nginx versions:
cat /usr/share/doc/roadiz/RELEASE
grep -E '^roadiz/(core|rozier)-bundle ' /usr/share/doc/roadiz/composer-packages.txt
php -v | head -1
mariadb --version
redis-server --version
nginx -v 2>&1
Roadiz 2.7.54 (roadiz/skeleton v2.7.9)
inventory-sha256 5ab4f98fcda8f4d209bdc74679171e37689b9e5b518c4c5785ccec9cd52ff4cb
packages 188
roadiz/core-bundle v2.7.54
roadiz/rozier-bundle v2.7.54
PHP 8.3.6 (cli) (built: Oct 19 2026 15:16:58) (NTS)
mariadb Ver 15.1 Distrib 10.11.14-MariaDB, for debian-linux-gnu (x86_64) using EditLine wrapper
Redis server v=7.0.15 sha=00000000:0 malloc=jemalloc-5.3.0 bits=64 build=d064f9f48c901a92
nginx version: nginx/1.24.0 (Ubuntu)
inventory-sha256 is the checksum of the installed package list (/usr/share/doc/roadiz/composer-packages.txt), the list that was checked for known vulnerabilities before the build.
Step 4: Get your sign-in details
The credentials file holds the address Roadiz is served as (your instance's public DNS name by default), the back-office and API addresses, the administrator's user name, password and e-mail, the database credentials and the health check token, all generated on this instance. Show it with:
sudo cat /root/roadiz-credentials.txt
ROADIZ_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
ROADIZ_ADMIN_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/rz-admin
ROADIZ_API_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/api/
ROADIZ_ADMIN_USER=admin
ROADIZ_ADMIN_PASSWORD=<24 letters and digits>
ROADIZ_ADMIN_EMAIL=admin@example.com
ROADIZ_DB_NAME=roadiz
ROADIZ_DB_USER=roadiz
ROADIZ_DB_PASSWORD=<48 hexadecimal characters>
ROADIZ_HEALTH_CHECK_TOKEN=<48 hexadecimal characters>
The administrator's e-mail starts as admin@example.com; change it to your own address in the back-office (Users) before you rely on password reset e-mails or login links.
Step 5: Sign in to the back-office
Open ROADIZ_ADMIN_URL in your browser (opening ROADIZ_URL takes you there too). The certificate is self signed (generated on your instance), so your browser warns once: compare the certificate's SHA-256 fingerprint with the one on the instance (sudo openssl x509 -in /etc/nginx/ssl/roadiz.crt -noout -fingerprint -sha256) and continue, or install a certificate from your own certificate authority (Step 11). Sign in with ROADIZ_ADMIN_USER and ROADIZ_ADMIN_PASSWORD:

The back-office opens on its dashboard, with the main menu on the left and the node tree beside it:

The administrator is a super administrator and can do everything. For editors, create accounts under Users and give them, or a group, only the roles they need.
Step 6: Nodes, documents and the content API
A fresh instance has two sample pages, Home and About, so the tree and the API are not empty. Nodes lists every node:

Open a node to edit its fields; each translation has its own tab:

Upload images, PDFs and other files under Documents; Roadiz stores them in public/files and serves resized versions from /assets/:

Users lists the accounts; the image has exactly one, the administrator:

Node types (the fields a page, a menu or a block has) are defined in code in Roadiz 2.7: they are the YAML files in /var/www/roadiz/config/node_types/, turned into PHP classes with the console. The back-office lists them but does not edit them. To change them, edit the YAML as root and regenerate (Step 12).
The content API is public for reading. The sample pages, as a website or app would fetch them:
H=$(sed -n 's/^set \$rz_host "\(.*\)";$/\1/p' /etc/nginx/roadiz-host.conf)
curl -sk --resolve "$H:443:127.0.0.1" -H 'Accept: application/json' "https://$H/api/nodes_sources" | python3 -c 'import json,sys; [print(n["@type"] if "@type" in n else "Page", n["title"]) for n in json.load(sys.stdin)]'
Page Home
Page About
Writing through the API needs a JWT. Ask /api/token for one with a back-office account (the password is read from a file here so it does not appear on the command line or in your shell history), then send it as a bearer token:
H=$(sed -n 's/^set \$rz_host "\(.*\)";$/\1/p' /etc/nginx/roadiz-host.conf)
sudo python3 -c 'import json; c=dict(l.strip().split("=",1) for l in open("/root/roadiz-credentials.txt") if l.startswith("ROADIZ_ADMIN_")); print(json.dumps({"username": c["ROADIZ_ADMIN_USER"], "password": c["ROADIZ_ADMIN_PASSWORD"]}))' > /tmp/rz-login.json
curl -sk --resolve "$H:443:127.0.0.1" -H 'Content-Type: application/json' --data-binary @/tmp/rz-login.json "https://$H/api/token" | python3 -c 'import json,sys; t=json.load(sys.stdin)["token"]; print("JWT received,", len(t), "characters")'
shred -u /tmp/rz-login.json
JWT received, 877 characters
The OpenAPI documentation of every endpoint is at https://<your host>/api/docs:

Your website or app on another origin needs a CORS rule: set CORS_ALLOW_ORIGIN in /var/www/roadiz/.env.local to a regular expression of its origins (for example "^https://(www\.)?example\.com$"), then clear the cache (Step 12). roadiz-set-hostname resets it to the instance's own name.
Step 7: Configuration and the health check
Roadiz's configuration for this instance is /var/www/roadiz/.env.local, written on first boot, readable by PHP and never writable by it:
sudo stat -c '%a %U:%G %n' /var/www/roadiz/.env.local
sudo grep -vE '^(#|APP_SECRET|DATABASE_URL|JWT_PASSPHRASE|APP_HEALTH_CHECK_TOKEN)' /var/www/roadiz/.env.local
640 root:www-data /var/www/roadiz/.env.local
APP_ENV=prod
APP_DEBUG=0
APP_NAMESPACE=roadiz
APP_TITLE="Roadiz"
APP_DESCRIPTION="Roadiz headless CMS"
REDIS_DSN=redis:///run/redis/redis-server.sock
MESSENGER_TRANSPORT_DSN=redis:///run/redis/redis-server.sock?stream=messages
MESSENGER_FAILED_TRANSPORT_DSN=redis:///run/redis/redis-server.sock?stream=failed
MAILER_DSN=null://null
APP_DEEPL_API_KEY=
APP_UNSPLASH_CLIENT_ID=
SENTRY_DSN=
OPEN_ID_DISCOVERY_URL=
APP_ROUTER_DEFAULT_URI=https://ec2-<public-ip-dashed>.compute-1.amazonaws.com
SYMFONY_TRUSTED_HOSTS="^ec2-<public-ip-dashed>\\.compute-1\\.amazonaws\\.com$"
CORS_ALLOW_ORIGIN="^https://ec2-<public-ip-dashed>\\.compute-1\\.amazonaws\\.com$"
APP_SECRET, DATABASE_URL (which holds the database password), JWT_PASSPHRASE and APP_HEALTH_CHECK_TOKEN are left out above; keep them secret. Other settings (site name, e-mail sender, maintenance mode) live in the database under Settings in the back-office.
The health check endpoint for monitoring answers only with this instance's token, sent as a header (nginx also answers /healthz with ok for load balancer checks):
H=$(sed -n 's/^set \$rz_host "\(.*\)";$/\1/p' /etc/nginx/roadiz-host.conf)
T=$(sudo sed -n 's/^ROADIZ_HEALTH_CHECK_TOKEN=//p' /root/roadiz-credentials.txt)
curl -sk -o /dev/null -w 'without the token: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/health-check"
curl -sk -H "x-health-check: $T" --resolve "$H:443:127.0.0.1" "https://$H/health-check"; echo
without the token: 404
{"status":"pass","version":"0.1.0","notes":{"roadiz_version":"2.7.54","roadiz_channel":"main"}}
Step 8: Your data
Everything that is yours is in three places: the MariaDB database roadiz, the uploaded documents in /var/www/roadiz/public/files (and private documents in /var/www/roadiz/var/files), and the configuration in /var/www/roadiz/.env.local with the JWT key pair in /var/www/roadiz/config/jwt/. A backup:
sudo mysqldump --single-transaction roadiz | gzip > roadiz-$(date +%F).sql.gz
sudo tar -C /var/www/roadiz -czf roadiz-files-$(date +%F).tar.gz public/files var/files .env.local config/jwt
Keep both backups secret: the database holds the users' password hashes and the archive holds .env.local and the JWT private key. An EBS snapshot of the volume covers everything at once. Resized images in public/assets and the cache in var/cache are rebuilt on demand.
Step 9: Security checks
Port 80 and the instance's IP address send browsers to the configured HTTPS host, the root address opens the back-office, and stray PHP addresses, the index.php/... form, .env.local, the project files outside public/, the JWT key and PHP in the document folder are not reachable:
H=$(sed -n 's/^set \$rz_host "\(.*\)";$/\1/p' /etc/nginx/roadiz-host.conf)
curl -s -o /dev/null -w 'port 80: %{http_code} -> %{redirect_url}\n' --resolve "$H:80:127.0.0.1" "http://$H/rz-admin" | sed -E 's#https://[^/]+#https://<your host>#'
IP=$(hostname -I | awk '{print $1}'); curl -sk -o /dev/null -w 'by IP address: %{http_code} -> %{redirect_url}\n' "https://$IP/rz-admin" | sed -E 's#https://[^/]+#https://<your host>#'
curl -sk -o /dev/null -w '/: %{http_code} -> %{redirect_url}\n' --resolve "$H:443:127.0.0.1" "https://$H/" | sed -E 's#https://[^/]+#https://<your host>#'
curl -sk --resolve "$H:443:127.0.0.1" "https://$H/healthz"
for p in /index.php /index.php/rz-admin/login /.env.local /config/jwt/private.pem /composer.json /vendor/autoload.php /bin/console /files/x.php /x.php; do
curl -sk -o /dev/null -w "$p: %{http_code}\n" --resolve "$H:443:127.0.0.1" "https://$H$p"
done
curl -s -o /dev/null -w 'unknown host name: %{http_code}\n' -H 'Host: example.com' http://127.0.0.1/ || true
port 80: 301 -> https://<your host>/rz-admin
by IP address: 301 -> https://<your host>/rz-admin
/: 302 -> https://<your host>/rz-admin
ok
/index.php: 404
/index.php/rz-admin/login: 404
/.env.local: 404
/config/jwt/private.pem: 404
/composer.json: 404
/vendor/autoload.php: 404
/bin/console: 404
/files/x.php: 404
/x.php: 404
unknown host name: 000
(000 means nginx closed the connection without an answer.) The credentials file is readable by root only, the configuration and the JWT key pair by root and the web server, the data folders belong to the web server, and the code belongs to root:
sudo stat -c '%a %U:%G %n' /root/roadiz-credentials.txt /var/www/roadiz/.env.local /var/www/roadiz/config/jwt /var/www/roadiz/config/jwt/private.pem /var/www/roadiz/var /var/www/roadiz/public/files /var/www/roadiz/public/assets /var/www/roadiz/public/index.php /var/www/roadiz/src
600 root:root /root/roadiz-credentials.txt
640 root:www-data /var/www/roadiz/.env.local
750 root:www-data /var/www/roadiz/config/jwt
640 root:www-data /var/www/roadiz/config/jwt/private.pem
750 www-data:www-data /var/www/roadiz/var
750 www-data:www-data /var/www/roadiz/public/files
750 www-data:www-data /var/www/roadiz/public/assets
644 root:root /var/www/roadiz/public/index.php
755 root:root /var/www/roadiz/src
An HTML file in the document folder is sent as a download with nosniff and the sandboxing policy, never rendered as a page of your site:
H=$(sed -n 's/^set \$rz_host "\(.*\)";$/\1/p' /etc/nginx/roadiz-host.conf)
echo '<b>demo</b>' | sudo -u www-data tee /var/www/roadiz/public/files/guide-demo.html >/dev/null
curl -sk -D - -o /dev/null --resolve "$H:443:127.0.0.1" "https://$H/files/guide-demo.html" | tr -d '\r' | grep -iE '^(HTTP|content-disposition|content-security-policy|x-content-type)'
sudo -u www-data rm -f /var/www/roadiz/public/files/guide-demo.html
HTTP/2 200
x-content-type-options: nosniff
content-security-policy: default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; sandbox
content-disposition: attachment
The certificate was generated on this instance, names its own addresses and can only vouch for itself (it is not a certificate authority):
H=$(sed -n 's/^set \$rz_host "\(.*\)";$/\1/p' /etc/nginx/roadiz-host.conf)
echo | openssl s_client -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | openssl x509 -noout -ext basicConstraints,extendedKeyUsage
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Extended Key Usage:
TLS Web Server Authentication
nginx, PHP, MariaDB, Redis, the background worker and the first boot service cannot reach the instance metadata service; PHP cannot run shell commands and is confined to the site folder; MariaDB has no network listener and Redis listens on a socket only; nginx hands PHP the configured host name, removes forwarding headers and rate-limits requests:
for u in nginx php8.3-fpm mariadb redis-server roadiz-messenger roadiz-firstboot; do systemctl show $u -p IPAddressDeny; done
php-fpm8.3 -i 2>/dev/null | grep -E '^(open_basedir|disable_functions|expose_php) ' | cut -d' ' -f1-3
sudo mysql -N -e 'SELECT @@skip_networking'
sudo redis-cli -s /run/redis/redis-server.sock config get port | tail -1
grep -E 'fastcgi_param (HTTP_HOST|SERVER_NAME|HTTP_X_FORWARDED_FOR|HTTP_CLIENT_IP|HTTP_X_FORWARDED_HOST|SCRIPT_FILENAME) ' /etc/nginx/snippets/cloudimg-roadiz-fastcgi.conf
grep -h 'limit_req_zone\|limit_req_status' /etc/nginx/conf.d/cloudimg-roadiz-guard.conf
IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32
IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32
IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32
IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32
IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32
IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32
disable_functions => exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec
expose_php => Off
open_basedir => /var/www/roadiz/:/var/lib/php/sessions/:/tmp/:/run/redis/:/run/mysqld/
1
0
fastcgi_param SCRIPT_FILENAME $realpath_root/index.php;
fastcgi_param HTTP_HOST $rz_host;
fastcgi_param SERVER_NAME $rz_host;
fastcgi_param HTTP_X_FORWARDED_HOST "";
fastcgi_param HTTP_X_FORWARDED_FOR "";
fastcgi_param HTTP_CLIENT_IP "";
limit_req_zone $rz_login_key zone=rz_login:10m rate=10r/m;
limit_req_zone $rz_post_key zone=rz_post:10m rate=120r/m;
limit_req_zone $rz_api_key zone=rz_api:10m rate=300r/m;
limit_req_zone $binary_remote_addr zone=rz_php:10m rate=600r/m;
limit_req_status 429;
rz_login counts back-office sign in, login link, password reset, /api/token and public form submissions from one address (10 a minute after a burst of 10), rz_post every request that sends data (120 a minute after a burst of 120), rz_api the content API under /api/ (300 a minute after a burst of 100) and rz_php every page (600 a minute after a burst of 300). Raise them in /etc/nginx/conf.d/cloudimg-roadiz-guard.conf (and the burst values in /etc/nginx/snippets/cloudimg-roadiz-fastcgi.conf) if many editors share one address, then sudo nginx -t && sudo systemctl reload nginx.
sudo roadiz-cred-roundtrip checks the whole sign in path on the instance: the redirect to HTTPS, that stray PHP and .env.local are refused, a sign in through the real back-office sign-in form and a JWT from the API with the password from the credentials file, and that a wrong and a common password are refused:
sudo roadiz-cred-roundtrip
OK https 301 + no stray PHP/.env + administrator sign-in + JWT + wrong and default passwords refused
Links opened from another site. /rz-admin/documents/random downloads a random image from Unsplash (when you configure an Unsplash key) and adds it to your documents as soon as it is opened. It only works when the request comes from your site itself: nginx decides this from the browser's Sec-Fetch-Site, Origin and Referer headers (a request with none of them is refused too). Every other back-office action that changes data needs a form submission with Roadiz's CSRF protection, and the back-office pages themselves stay linkable:
H=$(sed -n 's/^set \$rz_host "\(.*\)";$/\1/p' /etc/nginx/roadiz-host.conf)
curl -sk -o /dev/null -w 'random document link from another site: %{http_code}\n' -H 'Sec-Fetch-Site: cross-site' --resolve "$H:443:127.0.0.1" "https://$H/rz-admin/documents/random"
curl -sk -o /dev/null -w 'back-office sign in page from another site: %{http_code}\n' -H 'Sec-Fetch-Site: cross-site' --resolve "$H:443:127.0.0.1" "https://$H/rz-admin/login"
random document link from another site: 403
back-office sign in page from another site: 200
Passwords. Roadiz stores passwords with Symfony's password hasher (bcrypt). The administrator's generated 24 character password is out of reach of guessing; ask your users to choose long passphrases.
Step 10: Behind a proxy or load balancer
nginx removes X-Forwarded-For, X-Real-IP, Client-IP and Forwarded from every request, and Roadiz trusts no proxy, so its log and the sign-in throttling use the address of the connection. Behind an AWS load balancer that is the load balancer's address; nginx's own access log (/var/log/nginx/access.log) records it the same way. To record visitors' addresses instead, configure nginx's real_ip module for your load balancer's addresses only and set SYMFONY_TRUSTED_PROXIES in .env.local to the same addresses (never to REMOTE_ADDR or a wide range).
Put the load balancer's DNS name (or your own name for it) in place with sudo roadiz-set-hostname (Step 11): Roadiz accepts only that host name.
Step 11: Your own DNS name and certificate
Point a DNS record at the instance, then set the name Roadiz is served as. This mints a new self signed certificate for the name, updates nginx (which hands the name to Roadiz and redirects the instance's other addresses to it), the cross-site rules, SYMFONY_TRUSTED_HOSTS, APP_ROUTER_DEFAULT_URI and CORS_ALLOW_ORIGIN in .env.local, clears Roadiz's cache, updates the addresses in /root/roadiz-credentials.txt and keeps the name across reboots:
sudo roadiz-set-hostname <your-domain>
To use a certificate from your own certificate authority instead, copy it over the generated pair and reload nginx (running roadiz-set-hostname again replaces it with a self signed one):
sudo install -m 0644 <your-domain>.crt /etc/nginx/ssl/roadiz.crt
sudo install -m 0600 <your-domain>.key /etc/nginx/ssl/roadiz.key
sudo nginx -t && sudo systemctl reload nginx
Once browsers trust the certificate you can also add an HSTS header (add_header Strict-Transport-Security "max-age=31536000" always;) to /etc/nginx/snippets/cloudimg-roadiz-headers.conf. The image does not send one: browsers ignore HSTS from a site whose certificate they do not trust.
Step 12: Passwords, e-mail, the console and upgrades
roadiz-set-password gives the administrator admin a new random password through Roadiz's own user code (run as the web server's user, never as root), signs out every session and records the password in /root/roadiz-credentials.txt. Run it without arguments, or pass your own password (24 to 72 letters and digits) on stdin. Users change their own password from their profile in the back-office.
sudo roadiz-set-password
printf '%s\n' '<your-new-password>' | sudo roadiz-set-password --stdin
The console. Roadiz's command line is bin/console. Run it as the web server's user, never as root (root would leave cache files PHP cannot rewrite):
cd /var/www/roadiz && sudo -u www-data php bin/console list
sudo -u www-data php bin/console cache:clear
E-mail. Password resets, login links and form notifications need a mail server. Set MAILER_DSN in /var/www/roadiz/.env.local (for example MAILER_DSN=smtp://<user>:<password>@<smtp-host>:587), set the sender under Settings, then clear the cache.
Optional services. The DeepL translation assistant, Unsplash images, Sentry error reporting and OpenID sign in are off. To use one, set its variable in .env.local (APP_DEEPL_API_KEY, APP_UNSPLASH_CLIENT_ID, SENTRY_DSN, OPEN_ID_DISCOVERY_URL and the other OPEN_ID_* values) and clear the cache. Gravatar pictures are off in /var/www/roadiz/config/packages/roadiz_core.yaml (useGravatar: false).
Node types and code. The project code belongs to root. To change node types, edit /var/www/roadiz/config/node_types/*.yaml as root, regenerate the PHP classes and migrations with sudo php bin/console app:migrate in /var/www/roadiz (this is the one console command that writes code, so it runs as root), then fix the cache's ownership with sudo chown -R www-data:www-data /var/www/roadiz/var and run sudo -u www-data php bin/console cache:clear.
Upgrades. Roadiz releases are listed at https://github.com/roadiz/core-bundle-dev-app/releases. The image does not keep Composer; after a snapshot, install it (https://getcomposer.org/download/), run composer update 'roadiz/*' --with-all-dependencies --no-dev in /var/www/roadiz as root, then sudo -u www-data php bin/console doctrine:migrations:migrate, sudo -u www-data php bin/console cache:clear, and remove Composer again. Read the Roadiz changelog for the release first.
Step 13: Server components
| Component | Path |
|---|---|
| Roadiz project (root owned) | /var/www/roadiz |
| Configuration (this instance's secrets) | /var/www/roadiz/.env.local |
| JWT key pair | /var/www/roadiz/config/jwt/ |
| Uploaded documents, resized images | /var/www/roadiz/public/files, /var/www/roadiz/public/assets |
| Cache, private documents, exports | /var/www/roadiz/var |
| Database (MariaDB datadir) | /var/lib/mysql |
| Redis (cache, sessions, job queue) | /var/lib/redis, socket /run/redis/redis-server.sock |
| Background worker | roadiz-messenger.service |
| nginx site, request limits and cross-site link rules | /etc/nginx/sites-available/cloudimg-roadiz, /etc/nginx/conf.d/cloudimg-roadiz-guard.conf, /etc/nginx/conf.d/cloudimg-xsite-guard.conf, /etc/nginx/snippets/cloudimg-roadiz-*.conf |
| PHP settings | /etc/php/8.3/fpm/conf.d/99-cloudimg-roadiz.ini |
| MariaDB and Redis settings | /etc/mysql/mariadb.conf.d/99-cloudimg-roadiz.cnf, /etc/redis/cloudimg-roadiz.conf |
| Certificate and key | /etc/nginx/ssl/ |
| First boot script and helpers | /usr/local/sbin/roadiz-firstboot.sh, /usr/local/lib/cloudimg/roadiz/ |
| Helpers | /usr/local/sbin/roadiz-set-hostname, roadiz-set-password, roadiz-cred-roundtrip |
| Licence, release, package list and image notes | /usr/share/doc/roadiz/ |
| Credentials | /root/roadiz-credentials.txt (mode 0600) |
Step 14: Security recommendations
- Allow port 22 in the security group from your own networks only.
- Change the administrator e-mail to your own (Step 4), keep its password secret and rotate it if it may have been shared (Step 12).
- Keep
.env.local, the JWT private key and your backups secret (Step 8). - Use your own DNS name and a certificate your browsers trust (Step 11), and allow only your own front-end origins in
CORS_ALLOW_ORIGIN(Step 6). - Keep Roadiz on the newest release (Step 12).
- Keep the operating system patched; unattended security upgrades are on, and
sudo apt-get update && sudo apt-get upgradeapplies everything else. MariaDB, Redis and PHP's FPM, intl, xsl, zip and redis packages come from Ubuntu's universe repository;sudo pro security-status --esm-appsshows whether Ubuntu Pro (ESM Apps) has security updates for them (none were pending when the image was built).
Support and licensing
Roadiz is free software under the MIT licence (a copy is at /usr/share/doc/roadiz/LICENSE), and the bundled libraries carry their own open source licences. There is no per user or per CPU licence fee and no licence key. This image installs the unmodified upstream release through its official distribution, plus the cloudimg first boot, settings and helpers described above. The AMI price covers cloudimg's build, hardening and per-instance setup.
- Email: support@cloudimg.co.uk
- Website: https://www.cloudimg.co.uk
- Support: 24/7, with a 24 hour response SLA