Remark42 1.18.0 on AWS User Guide
Overview
This guide covers launching and using the Remark42 1.18.0 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg forums and social networks listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
Remark42 is a self hosted comment engine, a privacy friendly alternative to Disqus. You add a small script to your site's page template and every page gets its own comment thread: readers comment, reply, vote, edit for a few minutes after posting and format with Markdown, and you moderate (delete, pin, block, read-only threads), export and import, including from Disqus and WordPress. Remark42 is one Go program with its comment store (BoltDB) and its web widget built in.

The cloudimg image makes it safe to run on EC2:
- No default account and no baked secret. The image ships with no administrator password, no signing secret, no comment store and no TLS key. On your instance's first boot a 48 character
SECRET(Remark42 signs every reader's session with it and keys its hashes with it) and a 24 character administrator password are generated into a root only file. - Anonymous readers, no social logins. Readers sign in with a name of their choice and no password (Remark42's anonymous sign-in). No OAuth client secret ships, so every social login (GitHub, Google and the rest) and e-mail sign-in stay off until you add your own application keys (Step 9). Names such as
adminandmoderatorare reserved. - Only your sites can embed it. Remark42 answers cross-origin requests (CORS) and allows framing of its widget (
frame-ancestors) only for this instance and the sites you add withremark42-sites, never for*. Any other site's page that tries to post, vote or edit with a reader's session is refused. - The administrator plane refuses other sites. nginx refuses an admin API request that another web site's page sends from your browser, even with the right password.
- Sign-in is rate limited. nginx answers
429to repeated sign-ins and to repeated administrator password attempts from one address, on top of Remark42's own limiter. - The image proxy cannot reach inside. Remark42 can fetch images for comments through its own proxy. On this image the
remark42account is refused, at the kernel level and on every boot, any connection to the EC2 instance metadata service, link-local, private (RFC 1918), carrier grade NAT and reserved addresses and your VPC's own address ranges, and all IPv6. Public images still come through. - HTTPS from the first boot. nginx serves Remark42 on port 443 with a certificate generated on your instance's first boot; port 80 redirects to HTTPS. Nothing answers until first boot has finished.
- Remark42 listens on localhost only (
127.0.0.1:8080), runs as its own unprivileged user with no Linux capabilities, and takes the client address only from nginx. - Built from source, with current fixes. Remark42 1.18.0 is built from its tagged release source with Go 1.27.3, with two of its Go libraries raised to their fixed releases (
golang.org/x/netandgithub.com/klauspost/compress); the web widget is upstream's own 1.18.0 build, checked file by file. - Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: Remark42 1.18.0 (/opt/remark42/remark42, configuration in /etc/remark42/remark42.env, the secrets in /etc/remark42/secrets.env, comments, avatars, pictures and daily backups in /var/lib/remark42), nginx, the tools remark42-sites, remark42-admin, remark42-set-hostname and remark42-selftest, 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 internet access.
- A security group that allows TCP 22 (SSH) from your own network, and TCP 443 (Remark42) and TCP 80 (redirects to HTTPS) from your readers, which for a public website means from anywhere.
- A website whose page template you can edit, to embed the comment widget.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Remark42 1.18.0 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (Remark42 itself needs very little, so a smaller instance such as t3.small serves a busy blog), your key pair, a subnet 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=remark42}]'
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| Remark42 1.18.0 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
First boot sets the host name, generates the certificate, the SECRET and the administrator password; the egress backstop loads; then Remark42 and nginx start:
systemctl is-active remark42-firstboot cloudimg-remark42-egress remark42 nginx
sudo test -f /var/lib/cloudimg/remark42-firstboot.done && echo FIRSTBOOT_DONE
All four units are active and the first boot marker is present:
active
active
active
active
FIRSTBOOT_DONE
Only SSH and nginx listen on the network. Remark42 listens on localhost only:
sudo ss -lntpH | awk '{print $4, $6}' | sed -E 's/users:\(\("([^"]+)".*/\1/' | grep -v '127.0.0.5' | sort -u
0.0.0.0:22 sshd
0.0.0.0:443 nginx
0.0.0.0:80 nginx
127.0.0.1:8080 remark42
[::]:22 sshd
[::]:443 nginx
[::]:80 nginx
Step 4: Get your URL and administrator password
sudo cat /var/lib/cloudimg/remark42-credentials.txt
The file is readable by root only and holds the URL, the site ID and the administrator's user name and password:
# Remark42 1.18.0 - generated on first boot by remark42-firstboot.service
# The administrator password is unique to this instance; none was baked into the image. Keep this file secret.
REMARK42_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
REMARK42_SITE_ID=remark
REMARK42_ADMIN_USER=admin
REMARK42_ADMIN_PASSWORD=<REMARK42_ADMIN_PASSWORD>
#
# Readers comment through the widget you embed in your site (https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/web/ is a demo page). They sign in
# anonymously (a chosen name, no password); no social login is configured. The administrator is HTTP basic 'admin' with
# REMARK42_ADMIN_PASSWORD on the admin API (moderation, export, import), e.g.:
# sudo remark42-admin GET '/api/v1/admin/blocked?site=remark'
# Allow your site to embed the widget: sudo remark42-sites add https://your-blog.example
# Self-test: sudo remark42-selftest Change the served name: sudo remark42-set-hostname <your-domain>
# User guide: https://www.cloudimg.co.uk/guides/remark42-1-18-0-on-ubuntu-24-04-aws/
The URL uses the instance's public DNS name from EC2 instance metadata. Opening https://<public-ip>/ sends you on to that name.
The image includes a self test. Through nginx it checks the redirect, the health and configuration endpoints, the widget, the content security policy, that the admin API refuses no password, a wrong password and another site and accepts the right one, the cross-origin and unknown host guards, that the image proxy refuses instance metadata, the service's privileges, the egress backstop and the metadata block:
sudo remark42-selftest
SELFTEST-OK https 301 + healthz + ping + config v1.18.0-cloudimg (anonymous sign-in) + widget scripts carry https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com + CSP frame-ancestors without * and no X-Frame-Options + admin API 401 without credentials, 401 wrong password, 200 per-instance password + 403 cross-site admin + 403 cross-origin POST + 444 unknown host + no CORS for an unlisted origin + image proxy refused metadata (404) + remark42 uid remark42, no capabilities, NoNewPrivs, listening 127.0.0.1:8080 only, egress backstop loaded + metadata 000 inside remark42 and nginx (control 200)
Step 5: Try the demo page
Open REMARK42_URL followed by web/ (for example https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/web/) in your browser. The certificate is self signed, so your browser asks you to confirm it the first time (Step 11 replaces it). The page is Remark42's own demo with a live comment thread. Choose Sign In, enter a name of at least three letters or digits in the Anonymous form and choose Submit; then write a comment and choose Send:

A reader can reply, vote, edit their comment for five minutes and delete it; Sort by orders the thread. Each reader's anonymous identity comes from the name and the address they connect from, and it lasts as long as their browser keeps its session.
Step 6: Allow your website
This is the step that makes Remark42 work on your own site. Remark42 shows its widget inside your page in a frame and calls its API from your page. Both are allowed only for this instance and the sites you list, which is what stops anyone else embedding your comment server or acting with your readers' sessions. A new instance lists no site, so list prints nothing:
sudo remark42-sites list
Add each site that will embed comments, as an exact origin: https:// plus the host name, and a port if it is not 443. remark42-sites writes the list into Remark42's ALLOWED_HOSTS (its frame-ancestors policy) and into nginx's CORS and request guards, and restarts Remark42:
sudo remark42-sites add https://blog.example.com
sudo remark42-sites remove https://blog.example.com
A wildcard (* or https://*.example.com), an http:// origin, localhost and a URL with a path are refused. The trade-off: each listed site can frame the widget and call the API with a reader's session from its own pages, so list only sites you control, and add each host name your blog answers on (for example both https://example.com and https://www.example.com).
Step 7: Embed the widget in your site
Add this to your page template where the comments should appear, with the host from REMARK42_URL:
<div id="remark42"></div>
<script>
var remark_config = { host: 'https://comments.example.com', site_id: 'remark' };
</script>
<script>!function(e,n){for(var o=0;o<e.length;o++){var r=n.createElement("script"),c=".js",d=n.head||n.body;"noModule"in r?(r.type="module",c=".mjs"):r.async=!0,r.defer=!0,r.src=remark_config.host+"/web/"+e[o]+c,d.appendChild(r)}}(remark_config.components||["embed"],document);</script>
Each page gets its own thread, keyed by its URL. To show comment counts on an index page or the latest comments in a sidebar, add "counter" or "last-comments" to components as described in the Remark42 documentation. The widget keeps the reader's session in its own storage and sends it in a header, so it works when your site and the comment server are on different domains. Readers' browsers will refuse the self signed certificate, so install a trusted one (Step 11) before you go live.
Step 8: Moderate and manage comments
The administrator is the HTTP basic user admin with REMARK42_ADMIN_PASSWORD, and works through Remark42's admin API. remark42-admin calls it through nginx for you; it reads the password from the credentials file and never puts it on a command line. This lists the blocked readers (none on a new instance):
sudo remark42-admin GET '/api/v1/admin/blocked?site=remark'
null
Other things you can do the same way (<comment-id> and <user-id> come from the comment's JSON, for example /api/v1/find?site=remark&url=<page-url>&format=plain):
sudo remark42-admin DELETE '/api/v1/admin/comment/<comment-id>?site=remark&url=<page-url>'
sudo remark42-admin PUT '/api/v1/admin/user/<user-id>?site=remark&block=1&ttl=24h'
sudo remark42-admin PUT '/api/v1/admin/readonly?site=remark&url=<page-url>&ro=1'
sudo remark42-admin PUT '/api/v1/admin/pin/<comment-id>?site=remark&url=<page-url>&pin=1'
To moderate from inside the widget instead, a reader account must be named as an administrator with ADMIN_SHARED_ID. Anonymous identities depend on the address a reader connects from, so do this with a social or e-mail sign-in (Step 9): sign in, copy your user ID from your profile in the widget, and add ADMIN_SHARED_ID=<user-id> to /etc/remark42/remark42.env, then sudo systemctl restart remark42.
Step 9: Sign-in options and notifications
The image enables anonymous sign-in only (AUTH_ANON=true in /etc/remark42/remark42.env). Anonymous comments suit a blog that wants no accounts; they cannot be tied to a person, so moderation is how you deal with abuse. Readers who reach the instance from a private address (inside your VPC or over a VPN) all appear to Remark42 as the proxy, so two of them choosing the same name share one anonymous identity; for such readers use a social or e-mail sign-in.
To add GitHub, Google, Microsoft, Telegram or e-mail sign-in, or e-mail and Telegram notifications, register your own application with the provider, keep its secret out of remark42.env (that file is readable by everyone on the instance) and give it to Remark42 in a root only file:
sudo install -m 0600 -o root -g root /dev/null /etc/remark42/oauth.env
sudo nano /etc/remark42/oauth.env # AUTH_GITHUB_CID=... and AUTH_GITHUB_CSEC=...
sudo systemctl edit remark42 # [Service] then EnvironmentFile=/etc/remark42/oauth.env
sudo systemctl restart remark42
The provider's callback URL is REMARK42_URL followed by auth/github/callback (or google, and so on). The full list of settings is in the Remark42 parameters reference. If a notification target such as an SMTP relay or a webhook receiver is on a private address in your VPC, allow it in the egress list (Step 10).
Step 10: The image proxy and outbound connections
Remark42 fetches avatars from sign-in providers and, through /api/v1/img, images that readers link to. Because those addresses come from readers, the remark42 account is refused any connection to the instance metadata service, link-local, private, carrier grade NAT, benchmarking (198.18.0.0/15), IETF protocol (192.0.0.0/24), multicast and reserved addresses and this VPC's own IPv4 ranges (read from instance metadata on every boot), and every IPv6 connection. If the VPC ranges cannot be read, every new IPv4 connection of the remark42 account is refused until they can. Remark42 does not start without these rules:
sudo iptables -S CLOUDIMG-REMARK42-EGRESS | grep -c -- '-j REJECT'
sudo ip6tables -S CLOUDIMG-REMARK42-EGRESS | tail -n 1
sudo cat /run/cloudimg/remark42-vpc-cidrs
24
-A CLOUDIMG-REMARK42-EGRESS -j REJECT --reject-with icmp6-adm-prohibited
172.31.0.0/16
The count of refused IPv4 ranges includes your VPC's ranges, and the VPC range shown is your own. To let Remark42 reach a service on a private address (an SMTP relay or webhook receiver in your VPC), add its range, one CIDR per line, to /etc/cloudimg/remark42-egress-allow and reload; metadata and link-local addresses can never be allowed:
echo 10.20.30.40/32 | sudo tee -a /etc/cloudimg/remark42-egress-allow
sudo systemctl reload cloudimg-remark42-egress
Step 11: Use your own domain and certificate
- In your DNS, point
<your-domain>(an A record, for examplecomments.example.com) at the instance's public IP address. An Elastic IP keeps that address stable. - Tell the instance to use it. This sets the name nginx accepts, the request guards and Remark42's
REMARK_URL, and generates a new self signed certificate for it:
sudo remark42-set-hostname <your-domain>
- For a certificate your readers' browsers trust, put the certificate chain for
<your-domain>in/etc/ssl/remark42/remark42.crtand its private key in/etc/ssl/remark42/remark42.key(root only, mode 600), and runsudo systemctl reload nginx. Alternatively put the instance behind an Application Load Balancer with an AWS Certificate Manager certificate that forwards to port 443 on the instance.
Comments are stored against the page URL and the site ID, so changing the comment server's name keeps every thread.
Step 12: Data and backups
Everything Remark42 keeps is in /var/lib/remark42: var/remark.db (the comment store), avatars/, pictures/ (images readers upload) and backup/, where Remark42 writes a compressed export of every site each day and keeps the last ten. To take an export now:
sudo remark42-admin GET '/api/v1/admin/export?site=remark&mode=file' -o /root/remark42-export.json.gz
sudo ls -l /root/remark42-export.json.gz | awk '{print $1, $3, $NF}'
-rw-r--r-- root /root/remark42-export.json.gz
To restore it on a new instance, unpack it and send it to the admin import endpoint. An import replaces all of that site's comments:
sudo gunzip -k /root/remark42-export.json.gz
sudo remark42-admin POST '/api/v1/admin/import?site=remark&provider=native' --data-binary @/root/remark42-export.json
Use provider=disqus, provider=wordpress or provider=commento with an export file from those services to bring existing comments into a new instance, before readers start commenting. An EBS snapshot of the instance's volume is also a complete backup. Keep /etc/remark42/secrets.env with your backups if you want readers' sessions to survive a move; without it they simply sign in again.
Step 13: Security checks
nginx and Remark42 are blocked from the instance metadata service:
for u in nginx remark42; do printf '%s ' "$u"; systemctl show -p IPAddressDeny --value "$u" | tr ' ' '\n' | sort | tr '\n' ' '; echo; done
nginx 168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128
remark42 168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128
Remark42 runs as remark42 with no Linux capabilities and cannot gain any:
P=$(systemctl show -p MainPID --value remark42)
ps -o user= -p "$P"
grep -E '^(CapEff|CapBnd|CapAmb|NoNewPrivs)' /proc/"$P"/status
remark42
CapEff: 0000000000000000
CapBnd: 0000000000000000
CapAmb: 0000000000000000
NoNewPrivs: 1
The admin API refuses a request without the password and one that another site's page sends; a change from a site you have not listed is refused and gets no CORS answer; a request for a host name the instance does not serve gets no answer at all; and plain HTTP redirects:
H=$(sudo sed -n 's#^REMARK42_URL=https://\([^/]*\)/$#\1#p' /var/lib/cloudimg/remark42-credentials.txt)
curl -sk -o /dev/null -w 'admin API without the password: %{http_code}\n' --resolve "$H:443:127.0.0.1" -H "Origin: https://$H" "https://$H/api/v1/admin/blocked?site=remark"
curl -sk -o /dev/null -w 'admin API from another site: %{http_code}\n' --resolve "$H:443:127.0.0.1" -H 'Sec-Fetch-Site: cross-site' "https://$H/api/v1/admin/blocked?site=remark"
curl -sk -o /dev/null -w 'comment from an unlisted site: %{http_code}\n' --resolve "$H:443:127.0.0.1" -X POST -H 'Content-Type: application/json' -H 'Origin: https://evil.example' --data '{}' "https://$H/api/v1/comment?site=remark"
curl -sk -D - -o /dev/null --resolve "$H:443:127.0.0.1" -H 'Origin: https://evil.example' "https://$H/api/v1/config?site=remark" | grep -ci '^access-control-allow-origin' | sed 's/^/CORS headers for an unlisted site: /'
curl -sk -o /dev/null -w 'unknown host name: %{http_code}\n' --resolve evil.example:443:127.0.0.1 https://evil.example/ || true
curl -s -o /dev/null -w 'http: %{http_code}\n' --resolve "$H:80:127.0.0.1" "http://$H/web/"
admin API without the password: 401
admin API from another site: 403
comment from an unlisted site: 403
CORS headers for an unlisted site: 0
unknown host name: 000
http: 301
The secrets, the credentials, the TLS key and the comment store are readable by root (and the store by the remark42 service user) only. systemd reads the secrets file as root and hands SECRET and the password to Remark42 in its environment, which only remark42 and root can read:
P=$(systemctl show -p MainPID --value remark42)
sudo stat -c '%a %U:%G %n' /var/lib/cloudimg/remark42-credentials.txt /etc/remark42/secrets.env /etc/remark42/remark42.env /etc/ssl/remark42/remark42.key /var/lib/remark42 /var/lib/remark42/var/remark.db /proc/"$P"/environ
600 root:root /var/lib/cloudimg/remark42-credentials.txt
600 root:root /etc/remark42/secrets.env
644 root:root /etc/remark42/remark42.env
600 root:root /etc/ssl/remark42/remark42.key
700 remark42:remark42 /var/lib/remark42
600 remark42:remark42 /var/lib/remark42/var/remark.db
400 remark42:remark42 /proc/<pid>/environ
Other notes:
- The administrator password is in
/etc/remark42/secrets.env(ADMIN_PASSWD) and in the credentials file. To change it, edit both withsudo nano(24 letters and digits; the image checks the format before it starts Remark42) and runsudo systemctl restart remark42. ChangingSECRETsigns every reader out. - Rate limits. nginx allows about ten sign-ins a minute and twelve requests carrying an administrator password a minute from one address, with a small burst, and answers
429beyond that. - Client addresses. nginx replaces any
X-Forwarded-FororX-Real-IPa client sends with the address it connected from, and Remark42 trusts those headers only from nginx on127.0.0.1. Remark42's log records a keyed hash of the address, not the address. - NVMe host identity. No NVMe-oF host identity ships in the image. If you install
nvme-cli, its host NQN and host ID are your instance's own. - Who can reach it. Keep port 22 open only to your own network. Ports 80 and 443 serve your readers.
- The build.
/usr/share/doc/cloudimg-remark42/BUILD-INFO.txtrecords the release, the Go toolchain and the dependency updates;go.mod,go.sumandgo-version-m.txtnext to it list every library in the program, andweb-manifest.txtthe widget's files. - Logs. nginx logs are in
/var/log/nginx(without query strings); Remark42 logs to the journal (journalctl -u remark42).
Step 14: Keep it up to date
Ubuntu security updates install automatically. To apply all updates now:
sudo apt-get update && sudo apt-get upgrade -y
New Remark42 releases arrive as new versions of this listing, built and checked the same way. To move to one, take an export (Step 12), launch the new version, add your sites and import it. Read the Remark42 release notes before you move.
Support and licensing
Remark42 is open source under the MIT licence (Umputun and contributors); the licence text is in /usr/share/doc/cloudimg-remark42/, and the source code is at github.com/umputun/remark42. The Go and web libraries it is built with are open source, mainly under the MIT, Apache 2.0 and BSD licences; go-version-m.txt and web-manifest.txt in the same directory list them. There is no per user licence fee. The AMI price covers cloudimg's build, hardening, first boot setup and support.
- Email: support@cloudimg.co.uk
- Website: https://www.cloudimg.co.uk
- Support: 24/7, with a 24 hour response SLA
The other versions on this listing are on the forums and social networks guide.