Remark42 on Ubuntu 24.04 on Azure User Guide
Overview
This image runs Remark42 1.16.4, the open source self hosted commenting engine by Umputun, on Ubuntu 24.04 LTS. Remark42 gives any page a comment section that you own: you drop a small snippet of JavaScript into your template, and readers get threaded discussions, replies, voting, markdown, emoji and RSS, while you get a moderation plane to pin, block, verify and remove content.
It is deliberately small. The whole engine is a single static Go binary with the web frontend compiled into it, and the entire data tier is a BoltDB file on the VM itself. There is no separate database server to run, no cache tier, and no third party service holding your readers' conversations.
Secure by default. This image ships no working credential at all. On the first boot of every VM, remark42-firstboot.service generates two independent random values unique to that VM:
- a JWT signing key (
SECRET), which signs every authentication token - an admin password (
ADMIN_PASSWD), which authenticates the built inadminuser over HTTP Basic auth
The signing key matters more than it first appears: a key baked into a shared image would let anyone who deployed that same image forge an administrator session on every VM built from it. So neither value exists in the image. Remark42 is wired to refuse to start until both have been generated, on two independent levers: remark42.service declares a hard Requires dependency on the first boot rotation, and a preflight check re runs on every start and refuses a placeholder or a weak value, which also catches a half edited configuration rather than only a failed first boot.
Authentication without an external identity provider. Remark42 normally federates to OAuth providers such as Google or GitHub, each of which needs an OAuth application you register yourself. So that the appliance is usable the moment it boots, this image enables Remark42's built in anonymous provider: a reader picks a display name and comments, with no third party dependency. The administration plane is unaffected, because it is protected by the per VM admin password and signing key, and RESTRICTED_NAMES prevents an anonymous visitor claiming the admin name. Step 12 covers adding OAuth or email providers and turning anonymous access off for a locked down deployment.
What is included:
- Remark42 1.16.4, installed from the pinned upstream release binary, verified against its published SHA256 checksum at build time
- Remark42 bound to
127.0.0.1:8080, so the engine has no public listener of its own - nginx on port
80reverse proxying the engine, so it is reachable without a port suffix, plus an unauthenticated/healthzendpoint for Azure Load Balancer health probes - A per VM admin password and JWT signing key generated on first boot, with the password recorded at
/root/remark42-credentials.txt(mode0600) remark42.service,nginx.serviceand a one shotremark42-firstboot.serviceas systemd units, enabled and active- A fully patched Ubuntu 24.04 LTS base with unattended security upgrades enabled
- 24/7 cloudimg support
Prerequisites
An active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is comfortable for a busy site: Remark42 serves comments out of an embedded key value store and does no rendering work of its own. NSG inbound: allow 22/tcp from your management network and 80/tcp for readers. Port 8080 never needs to be opened, because the engine listens only on loopback and nginx is the sole way in.
Step 1 - Deploy from the Azure Marketplace
In the Azure portal, search the Marketplace for Remark42 on Ubuntu 24.04 LTS by cloudimg, select Create, and work through the wizard: choose your subscription, resource group and region, keep the recommended Standard_B2s size, select SSH public key authentication with username azureuser, and allow inbound 22 and 80. Review and create.
Step 2 - Deploy from the Azure CLI
az vm create \
--resource-group <your-rg> \
--name remark42 \
--image <marketplace-image-urn> \
--size Standard_B2s \
--admin-username azureuser \
--ssh-key-values ~/.ssh/id_ed25519.pub \
--vnet-name <your-vnet> --subnet <your-subnet> \
--public-ip-sku Standard
az vm open-port --resource-group <your-rg> --name remark42 --port 80 --priority 1010
Step 3 - Connect to your VM
ssh azureuser@<vm-public-ip>
Step 4 - Confirm the services are running
Three units make up the appliance: the one shot remark42-firstboot.service that generated this VM's credentials, the remark42.service engine itself, and nginx.service in front of it.
systemctl is-active remark42 nginx remark42-firstboot
sudo ss -tlnp | grep -E ':80 |:8080 '
Expected output:
active
active
active
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=8709,fd=5),...)
LISTEN 0 4096 127.0.0.1:8080 0.0.0.0:* users:(("remark42",pid=8700,fd=6))
Note the second line: Remark42 is bound to 127.0.0.1, not to a public interface. Everything reaches it through nginx.

Step 5 - Retrieve your per VM admin password
The password generated on this VM's first boot is recorded in a root only file. The command below masks the value so you can read the file safely on a shared screen:
sudo ls -l /root/remark42-credentials.txt
sudo sed -E 's/^(REMARK42_ADMIN_PASSWORD=).*/\1********/' /root/remark42-credentials.txt | head -12
Expected output:
-rw------- 1 root root 1121 Jul 16 15:57 /root/remark42-credentials.txt
# Remark42 - generated on first boot by remark42-firstboot.service
# These values are unique to this VM. Store them somewhere safe.
REMARK42_URL=http://20.51.157.213/
REMARK42_ADMIN_USER=admin
REMARK42_ADMIN_PASSWORD=********

The password is written exactly once in that file, so the sed above fully redacts it for a screenshot or a support ticket. The JWT signing key is not repeated there at all: it lives only in /etc/remark42/remark42.env, because it signs every token and belongs in exactly one place.
Step 6 - Confirm the engine is serving
nginx serves an unauthenticated /healthz endpoint for load balancer probes. Remark42 publishes its version and enabled authentication providers at /api/v1/config, and the admin API requires the password from Step 5, so an unauthenticated request is rejected with 401 while an authenticated one returns 200:
curl -sI http://127.0.0.1/healthz | head -1
curl -s 'http://127.0.0.1/api/v1/config?site=remark' | jq -r '.version, (.auth_providers|tostring)'
PW=$(sudo grep '^REMARK42_ADMIN_PASSWORD=' /root/remark42-credentials.txt | cut -d= -f2-)
curl -s -o /dev/null -w "no auth -> HTTP %{http_code}\n" 'http://127.0.0.1/api/v1/admin/blocked?site=remark'
curl -s -o /dev/null -w "with pw -> HTTP %{http_code}\n" -u "admin:$PW" 'http://127.0.0.1/api/v1/admin/blocked?site=remark'
Expected output:
HTTP/1.1 200 OK
v1.16.4-6e7820d2-20260710T031048
["anonymous"]
no auth -> HTTP 401
with pw -> HTTP 200

Reading the password into $PW rather than typing it keeps it out of your shell history and scrollback. You can confirm which identity the password authenticates as:
PW=$(sudo grep '^REMARK42_ADMIN_PASSWORD=' /root/remark42-credentials.txt | cut -d= -f2-)
curl -s -u "admin:$PW" 'http://127.0.0.1/api/v1/user?site=remark' | jq -c '{id, name}'
Step 7 - Confirm no default credential shipped
Remark42 ships no default password upstream, and this image adds none. Before first boot, both secrets sit in the configuration as a non secret placeholder token, and the engine will not start while that token is present. Prove it for yourself: the placeholder appears nowhere in the running configuration, it does not authenticate, and remark42.service declares a hard Requires dependency on the first boot rotation:
sudo grep -c CLOUDIMG_SET_ON_FIRSTBOOT /etc/remark42/remark42.env
curl -s -o /dev/null -w "admin:CLOUDIMG_SET_ON_FIRSTBOOT -> HTTP %{http_code}\n" -u admin:CLOUDIMG_SET_ON_FIRSTBOOT 'http://127.0.0.1/api/v1/admin/blocked?site=remark'
systemctl show remark42 -p Requires --value
Expected output:
0
admin:CLOUDIMG_SET_ON_FIRSTBOOT -> HTTP 401
remark42-firstboot.service system.slice sysinit.target -.mount

Step 8 - Set your public URL
This is the one setting you should change before going live. Remark42 builds every avatar URL, picture URL and emitted link by concatenating REMARK_URL, so it must be the address your readers use. First boot resolves it to the VM's public IP, which is correct for a quick look, but a real deployment sits behind your own name:
sudo sed -i -E 's#^REMARK_URL=.*#REMARK_URL=https://comments.example.com#' /etc/remark42/remark42.env
sudo systemctl restart remark42
Check what it is currently set to at any time:
grep '^REMARK_URL=' /etc/remark42/remark42.env
If you serve Remark42 over HTTPS, terminate TLS at nginx on this VM (or at an Azure Application Gateway in front of it) and set REMARK_URL to the https:// address. Avatar URLs already stored in existing comments keep the value that was in force when the comment was written, so it is worth setting this before you collect real traffic.
Step 9 - Open the comment interface
Browse to http://<vm-public-ip>/. The bare root redirects to /web/, which is Remark42's own demo page with a live comment thread on it. Readers see threaded discussion, avatars, relative timestamps, per comment scores and a sort control.

Step 10 - Sign in and post a comment
Select Sign In under the comment box. Because this image enables the built in anonymous provider, readers can pick a display name and start commenting immediately, with no OAuth application to register and no account to create.

Once signed in, the composer shows the signed in identity and a markdown toolbar, with Preview and Send. Comments support markdown, code blocks, emoji and an editing grace period, and readers can subscribe to a thread by RSS.

The widget follows the host page's colour scheme and ships a dark theme:

Step 11 - Embed the widget in your own site
Add this to the template of any page you want comments on, replacing the host with your own REMARK_URL from Step 8, then place a <div id="remark42"></div> where the thread should appear:
<script>
var remark_config = {
host: "https://comments.example.com",
site_id: "remark",
components: ["embed"]
};
(function(c) {
for (var i = 0; i < c.length; i++) {
var d = document, s = d.createElement("script");
s.src = remark_config.host + "/web/" + c[i] + ".js";
s.defer = true;
(d.head || d.body).appendChild(s);
}
})(remark_config.components || ["embed"]);
</script>
<div id="remark42"></div>
site_id is the site name Remark42 was started with, which is remark on this image. The counter component renders a comment count for an index page, and last-comments renders a recent comments list, so a documentation tree can show counts on the index and full threads on each article.
Step 12 - Moderate from the admin API
The admin password authenticates the built in admin user against every endpoint under /api/v1/admin/. List blocked users, block someone for a period, or pin a comment:
PW=$(sudo grep '^REMARK42_ADMIN_PASSWORD=' /root/remark42-credentials.txt | cut -d= -f2-)
curl -s -u "admin:$PW" 'http://127.0.0.1/api/v1/admin/blocked?site=remark' | jq -c .
Remark42 also scores comments. Anything that falls below the low score threshold is collapsed automatically, and you can pin the useful answers to the top of a thread. Export the whole site as a backup file at any time:
PW=$(sudo grep '^REMARK42_ADMIN_PASSWORD=' /root/remark42-credentials.txt | cut -d= -f2-)
curl -s -u "admin:$PW" 'http://127.0.0.1/api/v1/admin/export?site=remark&mode=file' -o /tmp/remark42-export.gz
ls -lh /tmp/remark42-export.gz
Step 13 - Add an OAuth provider and lock down anonymous access
For a public site you will usually want readers to sign in with a real identity. Register an OAuth application with your provider, set its callback URL to <your REMARK_URL>/auth/github/callback, then add the credentials and turn anonymous access off:
sudo tee -a /etc/remark42/remark42.env >/dev/null <<'EOF'
AUTH_GITHUB_CID=your-client-id
AUTH_GITHUB_CSEC=your-client-secret
EOF
sudo sed -i 's/^AUTH_ANON=true/AUTH_ANON=false/' /etc/remark42/remark42.env
sudo systemctl restart remark42
Remark42 supports Google, GitHub, Microsoft, Facebook, Yandex, Twitter, Patreon, Discord, a custom OAuth2 provider, Telegram, and email based sign in. /api/v1/config reports which providers are live, so you can confirm the change took:
curl -s 'http://127.0.0.1/api/v1/config?site=remark' | jq -r '.auth_providers|tostring'
To make a signed in user an administrator, add their Remark42 user ID to ADMIN_SHARED_ID in the same file and restart. The admin basic auth credential keeps working alongside it as a break glass path.
Maintenance
The whole data tier is a BoltDB file plus the avatar and image directories, all under /var/lib/remark42/var, owned by the unprivileged remark42 service account:
sudo ls -lh /var/lib/remark42/var/
That means a backup really is a file copy: stop the service, copy the directory, start it again. Remark42 also writes its own periodic backups into /var/lib/remark42/var/backup, and the export endpoint in Step 12 produces a portable dump. The Ubuntu base has unattended security upgrades enabled, so OS patches apply automatically. To move to a newer Remark42 release, watch the upstream releases feed: this image pins a specific verified binary rather than tracking a moving tag.
Support
cloudimg provides 24/7 support for this image. Contact support@cloudimg.co.uk with your Azure subscription ID and the VM name.