Deployment guide

Roadiz CMS 2.7.54

Overview

This guide covers launching and using the Roadiz CMS 2.7.54 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud CMS, Blogs and Wikis listing on AWS Marketplace. You choose 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. Roadiz also provides documents and folders with image resizing, tags, translations, custom forms, user groups and roles, a revision history and webhooks.

What you get:

  • 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 and 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. It then 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/skeleton 2.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 audit reports none). 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. 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 .php address, the index.php/... form, dotfiles such as .env.local, and everything outside the public folder are never served. The folders PHP writes uploaded documents (/files/) and resized images (/assets/) to are static only. Every file gets nosniff and a sandboxing content security policy. 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 limits requests from one address. It allows 10 back-office sign-in, login link, password reset, API token and public form submissions a minute (after a burst of 10), 300 content API requests a minute and 600 PHP requests a minute. It uses 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 removes X-Forwarded-For, Client-IP and 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 example 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. 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.local is readable by PHP but never writable, and only var/, public/files and public/assets are writable. PHP cannot run shell commands and is confined to the site folder. MariaDB has no network listener at all. 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 is patched 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. Limit port 22 to your own networks. Open port 443 to your editors and to the servers or apps that read the content API.
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, choose your Region, then choose Launch through EC2.

Select m5.large, your key pair and a security group as described above.

To launch with the AWS CLI instead, 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}]'
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>
3

Check the services

First boot takes about a minute. During that time it creates the database, generates the secrets and the JWT key pair, and installs Roadiz. Afterwards 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"))

Check 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). That is the list that was checked for known vulnerabilities before the build.

4

Get your sign-in details

The credentials file holds everything generated on this instance. It lists 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. 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 under Users before you rely on password reset e-mails or login links.

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 and was 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. Alternatively, 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.

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. Open Nodes to list 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/:

Open Users to list the accounts. The image has exactly one, the administrator:

Node types define the fields a page, a menu or a block has. In Roadiz 2.7 they are defined in code: 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. These are 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. Request one from /api/token with a back-office account, then send it as a bearer token. The commands below read the password from a file, so it does not appear on the command line or in your shell history:

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:

A website or app on another origin needs a CORS rule. Set CORS_ALLOW_ORIGIN in /var/www/roadiz/.env.local to a regular expression that matches its origins (for example "^https://(www\.)?example\.com$"), then clear the cache (Step 12). Running roadiz-set-hostname resets the value to the instance's own name.

7

Configuration and the health check

Roadiz's configuration for this instance is /var/www/roadiz/.env.local. First boot writes it, PHP can read it, and PHP can never write to 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= <set-at-first-boot>= 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$"

The output leaves out APP_SECRET, DATABASE_URL (which holds the database password), JWT_PASSPHRASE and APP_HEALTH_CHECK_TOKEN. Keep these 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 when you send this instance's token 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"}}
8

Your data

Your data lives in three places:

1. The MariaDB database roadiz.

2. The uploaded documents in /var/www/roadiz/public/files, and private documents in /var/www/roadiz/var/files.

3. The configuration in /var/www/roadiz/.env.local, with the JWT key pair in /var/www/roadiz/config/jwt/.

Back them up with:

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.

9

Security checks

Port 80 and the instance's IP address send browsers to the configured HTTPS host, and the root address opens the back-office. 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

The status 000 means nginx closed the connection without an answer.

The credentials file is readable by root only. The configuration and the JWT key pair are readable 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. It is 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 and names its own addresses. It can only vouch for itself, because 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

This check confirms the following:

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/sac-roadiz-fastcgi.conf grep -h 'limit_req_zone\|limit_req_status' /etc/nginx/conf.d/sac-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;

The four limit zones work as follows:

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 counts every request that sends data (120 a minute after a burst of 120). rz_api counts content API requests under /api/ (300 a minute after a burst of 100). rz_php counts every page (600 a minute after a burst of 300).

If many editors share one address, raise the limits in /etc/nginx/conf.d/sac-roadiz-guard.conf and the burst values in /etc/nginx/snippets/sac-roadiz-fastcgi.conf. Then run sudo nginx -t && sudo systemctl reload nginx.

sudo roadiz-cred-roundtrip checks the whole sign-in path on the instance. It verifies the redirect to HTTPS and that stray PHP and .env.local are refused. It signs in through the real back-office sign-in form, requests a JWT from the API with the password from the credentials file, and checks that a wrong password 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, and 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. 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.

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. Its log and the sign-in throttling therefore 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. Then set SYMFONY_TRUSTED_PROXIES in .env.local to the same addresses, never to REMOTE_ADDR or a wide range.

Set the load balancer's DNS name (or your own name for it) with sudo roadiz-set-hostname (Step 11). Roadiz accepts only that host name.

11

Your own DNS name and certificate

Point a DNS record at the instance, then set the name Roadiz is served as. The command below does the following:

It mints a new self-signed certificate for the name. It updates nginx, which hands the name to Roadiz and redirects the instance's other addresses to it, and the cross-site rules. It updates SYMFONY_TRUSTED_HOSTS, APP_ROUTER_DEFAULT_URI and CORS_ALLOW_ORIGIN in .env.local. It 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/sac-roadiz-headers.conf. The image does not send one, because browsers ignore HSTS from a site whose certificate they do not trust.

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, which runs as the web server's user and never as root. It also 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, because root would leave cache files that 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. Then 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.

Afterwards, 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. Read the Roadiz changelog for the release first. The image does not keep Composer, so take a snapshot and install it from https://getcomposer.org/download/.

Then, as root in /var/www/roadiz, run composer update 'roadiz/*' --with-all-dependencies --no-dev. Follow it with sudo -u www-data php bin/console doctrine:migrations:migrate and sudo -u www-data php bin/console cache:clear, and remove Composer again.

13

Server components

These are the component locations on the instance:

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/sac-roadiz, /etc/nginx/conf.d/sac-roadiz-guard.conf, /etc/nginx/conf.d/sac-xsite-guard.conf, /etc/nginx/snippets/sac-roadiz-*.conf

PHP settings: /etc/php/8.3/fpm/conf.d/99-sac-roadiz.ini

MariaDB and Redis settings: /etc/mysql/mariadb.conf.d/99-sac-roadiz.cnf, /etc/redis/sac-roadiz.conf

Certificate and key: /etc/nginx/ssl/

First-boot script and helpers: /usr/local/sbin/roadiz-firstboot.sh, /usr/local/lib/sac/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)

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 the 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). 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 upgrade applies 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-apps shows 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 SecureAnyCloud first-boot service, settings and helpers described above. The AMI price covers SecureAnyCloud's build, hardening and per-instance setup.

  • Email: sales@secureanycloud.com
  • Website: https://secureanycloud.com
  • Support: 24/7, with a 24-hour response SLA

Deploy on AWS

Launch Roadiz CMS 2.7.54 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.

View on Marketplace

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on AWS