GoAlert
Overview
GoAlert is the open source on-call scheduling and alerting platform originally built at Target. It tracks who is on call (rotations and schedules), decides who gets paged next (escalation policies with delays and repeats), and turns signals from your monitoring into alerts that people acknowledge, escalate and close.
Monitoring systems raise alerts through per-service integration keys (a generic HTTP API, email, Grafana, Prometheus Alertmanager and Site24x7). GoAlert notifies people by SMS and voice (Twilio), email (SMTP), Slack and webhooks.
This image runs GoAlert 0.35.0 on Ubuntu 24.04 LTS, compiled from source and hardened for first-boot setup.
What you get:
- HTTPS from the first boot. nginx serves GoAlert on port 443 with a certificate generated on your VM's first boot. Port 80 redirects to HTTPS, and requests for any host name the VM does not serve are dropped.
- No default login. GoAlert itself ships without users. First boot creates a single
adminaccount with a password unique to your VM, stored only as a bcrypt hash and in a root-only file. The password is handed to GoAlert on its own password prompt, never on a command line, so it never appears in the process list. - A per-VM encryption key. GoAlert encrypts its stored secrets (provider tokens, signing keys) with a data encryption key that first boot generates for your VM alone and keeps in a root-only environment file.
- A database with no password and no network port. First boot creates the PostgreSQL 16 cluster on your VM. PostgreSQL listens on no TCP port at all. GoAlert reaches it over the local socket as its own system user (peer authentication), so there is no database password to leak.
- Fail closed. GoAlert and nginx refuse to start until first boot has finished, so the web interface is never reachable without its admin account and certificate.
- Built from source and vulnerability scanned. This image compiles the GoAlert 0.35.0 source with Go 1.27.3 and patched Go modules. govulncheck reports no known vulnerability in the shipped binary (see Step 11).
- Hardened defaults. GoAlert listens on loopback only and runs as an unprivileged user in a systemd sandbox. GoAlert, nginx and PostgreSQL are all blocked from the Azure Instance Metadata Service and the Azure wire server.
- Fully patched base. Ubuntu 24.04 LTS is patched at build time, with unattended security upgrades switched on.
- 24/7 SecureAnyCloud support
What changed in this image version. Earlier versions of this image ran the upstream 0.34.1 release binary over plain HTTP. This version:
- Compiles GoAlert 0.35.0 from source with Go 1.27.3 and patched
golang.org/x/netandgolang.org/x/crypto - Serves HTTPS with a per-VM certificate, redirects port 80 to HTTPS and drops unknown host names
- Creates the admin through GoAlert's own password prompt, so no password, database URL or encryption key is ever on a command line
- Runs PostgreSQL with no TCP listener and no database password (peer authentication over the local socket)
- Passes GoAlert the real client address from the connection, never a client-supplied
X-Forwarded-For - Refuses cross-site state-changing requests and WebSocket upgrades
- Answers the
/healthzhealth check on the VM itself only - Blocks GoAlert, nginx and PostgreSQL from the Azure metadata service and wire server
- Masks fwupd's firmware refresh timer and service, because an Azure VM has no firmware that LVFS can update (the fwupd package stays installed)
What is on the image: GoAlert 0.35.0 (/usr/local/bin/goalert, compiled from source), PostgreSQL 16, nginx, Ubuntu 24.04 LTS, cloud-init and the Azure Linux agent.
Recommended VM size: Standard_B2s (2 vCPU, 4 GiB) or larger.
Prerequisites
You need:
- An active Azure subscription and an SSH key pair (or a password, if you choose password authentication in the portal)
- A network security group that allows TCP 22 (SSH), TCP 443 (the web interface and the alert APIs) and TCP 80 (redirects to HTTPS) from your own networks and monitoring systems only
Deploy from the Azure Marketplace
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for GoAlert by SecureAnyCloud and select Create.
On the Basics tab, pick your subscription, resource group, region and size, and set the administrator account (SSH public key or password). Under Inbound port rules, allow SSH (22), HTTPS (443) and HTTP (80).
Select Review + create, then Create.
Deploy from the Azure CLI
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:goalert-ubuntu-24-04:default:latest
az vm create \
--resource-group <your-rg> \
--name goalert \
--image sac1647283583153:goalert-ubuntu-24-04:default:latest \
--size Standard_B2s \
--admin-username azureuser \
--ssh-key-values ~/.ssh/id_ed25519.pub \
--public-ip-sku Standard
az vm open-port --resource-group <your-rg> --name goalert --port 443 --priority 1010
az vm open-port --resource-group <your-rg> --name goalert --port 80 --priority 1020First boot takes about a minute. It creates the database, the admin account, the encryption key and the certificate.
Connect and check the services
ssh azureuser@<vm-public-ip>Confirm that PostgreSQL, GoAlert and nginx are active, and that the first-boot and per-boot host units have completed:
systemctl is-active postgresql@16-main goalert nginx goalert-firstboot goalert-configure
sudo test -f /var/lib/sac/data/goalert-firstboot.done && echo FIRSTBOOT_DONEactive active active active active FIRSTBOOT_DONEOnly SSH and nginx listen on public addresses. GoAlert listens on loopback, and PostgreSQL has no TCP listener at all. Confirm the listeners:
sudo ss -lntp | awk 'NR>1 {print $4, $6}' | grep -v '127.0.0.5' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort0.0.0.0:22 users:(("sshd"),("systemd")) 0.0.0.0:443 users:(("nginx"),("nginx"),("nginx")) 0.0.0.0:80 users:(("nginx"),("nginx"),("nginx")) 127.0.0.1:8081 users:(("goalert")) [::]:22 users:(("sshd"),("systemd")) [::]:443 users:(("nginx"),("nginx"),("nginx")) [::]:80 users:(("nginx"),("nginx"),("nginx"))Get your URL and admin password
sudo cat /root/goalert-credentials.txtThe file is readable by root only. It holds the sign-in URL, the user name and this VM's password:
# GoAlert - generated on first boot by goalert-firstboot.service
# These credentials are unique to this VM. Store them somewhere safe. GOALERT_URL=https://<vm-public-ip>/ GOALERT_ADMIN_USER=admin GOALERT_ADMIN_PASSWORD=<GOALERT_ADMIN_PASSWORD>
#
# Open GOALERT_URL in a browser (self-signed certificate minted on this VM's first boot; port 80 redirects # to https) and sign in with the username and password above. GoAlert only accepts sign-ins made from that URL. # Serve it under your own DNS name:
sudo goalert-set-hostname <your-dns-name>
# Alert delivery (SMS, voice, email, Slack, webhooks) is configured under Admin -> Config after signing in.
# Full deployment guide: https://secureanycloud.com/stacks/goalert/The URL uses the VM's public IP, which first boot reads from the Azure Instance Metadata Service. GoAlert accepts a sign-in only from the URL it is configured with (it checks the browser's Referer), so always open this URL rather than another name or address. The certificate also names the VM's private IP.
If you deallocate and start the VM and its public IP changes, the next boot re-reads the IP and updates the URL, the certificate and /root/goalert-credentials.txt for you. To use your own DNS name instead, see Step 9.
Sign in from your browser
Open the GOALERT_URL in your browser. The certificate is self-signed, so your browser asks you to confirm it the first time (or install your own certificate, Step 9). Sign in as admin with the password from Step 4:
The Alerts page lists open alerts for the services you are on call for or have marked as favourites. Open an alert to acknowledge, escalate or close it, and to see which escalation step is paging whom:
A service ties an escalation policy to the integration keys your monitoring uses to raise alerts:
Schedules combine rotations, rules and temporary overrides into a calendar of who is on call:
A typical first setup follows this order:
- Users: invite your team, or switch on GitHub or OIDC sign-in under Admin.
- Rotations and Schedules: define who is on call and when.
- Escalation Policies: define which schedule or person is paged first, and after how many minutes the next step follows.
- Services: add one with an integration key for each monitoring system.
Each user adds their own contact methods and notification rules under their profile. The Wizard in the menu creates a service, an escalation policy and a schedule in one go.
Raise a test alert from the command line
Port 80 redirects to HTTPS, the health check answers only on the VM itself, GoAlert's internal health endpoints are not exposed, and other host names are dropped. Verify this behaviour:
H=$(sudo grep '^GOALERT_URL=' /root/goalert-credentials.txt | sed -E 's#^GOALERT_URL=https://([^/]+)/.*#\1#')
P=$(hostname -I | awk '{print $1}')
curl -s -o /dev/null -w 'http:// -> %{http_code} %{redirect_url}\n' --resolve "$H:80:127.0.0.1" "http://$H/alerts" echo "/healthz (local) -> $(curl -sk --resolve "$H:443:127.0.0.1" "https://$H/healthz")"
curl -sk -o /dev/null -w '/healthz (other) -> HTTP %{http_code}\n' --interface "$P" --resolve "$H:443:$P" "https://$H/healthz"
curl -sk -o /dev/null -w '/health -> HTTP %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/health"
curl -sk -o /dev/null -w 'other Host -> %{http_code} (connection closed)\n' -H 'Host: example.com' https://127.0.0.1/ || truehttp:// -> 301 https://<vm-public-ip>/alerts /healthz (local) -> ok /healthz (other) -> HTTP 403 /health -> HTTP 404 other Host -> 000 (connection closed)The script below signs in as admin and creates a Web shop service whose escalation policy pages you. It gives the service a generic API integration key and raises an alert through that key, exactly as a monitoring system would.
The password goes to curl on standard input, and the integration key travels in an Authorization: Bearer header. Neither appears in the process list, your shell history or the web server's access log:
sudo bash <<'EOF_GOALERT' set -euo pipefail F=/root/goalert-credentials.txt
P=$(grep '^GOALERT_ADMIN_PASSWORD=' "$F" | cut -d= -f2-)
H=$(grep '^GOALERT_URL=' "$F" | sed -E 's#^GOALERT_URL=https://([^/]+)/.*#\1#') B="https://$H"; R=(--resolve "$H:443:127.0.0.1");
J=$(mktemp); trap 'rm -f "$J"' EXIT
# Sign in as admin: the form body goes to curl on stdin, so the password never appears in the process list printf 'username=admin&password=%s' "$P" | curl -sk "${R[@]}" -o /dev/null -c "$J" -H "Referer: $B/" --data-binary @- "$B/api/v2/identity/providers/basic" gql() { curl -sk "${R[@]}" -b "$J" -H "Referer: $B/" -H "Origin: $B" -H 'Content-Type: application/json' --data-binary @- "$B/api/graphql"; }
ME=$(printf '%s' '{"query":"{user{id name role}}"}' | gql | jq -r '.data.user.id') [ -n "$ME" ] && [ "$ME" != null ] || { echo "sign-in failed"; exit 1; }
# A service whose escalation policy pages you, with a generic API integration key (reused if it already exists)
S=$(printf '%s' '{"query":"{services(input:{search:\"Web shop\"}){nodes{id name integrationKeys{id type}}}}"}' | gql)
SVC=$(jq -c '[.data.services.nodes[] | select(.name == "Web shop")][0] // empty' <<<"$S") if [ -z "$SVC" ]; then
Q=$(jq -nc --arg me "$ME" '{query:"mutation($in:CreateServiceInput!){createService(input:$in){id name integrationKeys{id type}}}",variables:{in:{name:"Web shop",newEscalationPolicy:{name:"Web shop on-call",steps:[{delayMinutes:5,targets:[{id:$me,type:"user"}]}]},newIntegrationKeys:[{type:"generic",name:"monitoring"}]}}}')
SVC=$(printf '%s' "$Q" | gql | jq -c '.data.createService // empty') fi
SID=$(jq -r '.id // empty' <<<"$SVC")
KEY=$(jq -r '[.integrationKeys[] | select(.type == "generic")][0].id // empty' <<<"$SVC") [ -n "$SID" ] && [ -n "$KEY" ] || { echo "service creation failed: $SVC"; exit 1; } echo "service: $(jq -r '.name' <<<"$SVC") (integration key type generic)"
# Raise an alert the way a monitoring system would: the key travels in an Authorization header
C=$(printf 'Authorization: Bearer %s\n' "$KEY" | curl -sk "${R[@]}" -o /dev/null -w '%{http_code}' -H @- -X POST "$B/api/v2/generic/incoming" \
--data-urlencode 'summary=Checkout latency above 2s' --data-urlencode 'details=p95 2.4 s for 5 minutes on web-1')
echo "generic API: HTTP $C" [ "$C" = 204 ] || [ "$C" = 200 ] || { echo "alert was not accepted"; exit 1; } for i in $(seq 1 10); do
A=$(jq -nc --arg s "$SID" '{query:"query($s:ID!){alerts(input:{filterByServiceID:[$s]}){nodes{alertID status summary}}}",variables:{s:$s}}' | gql) [ "$(jq '.data.alerts.nodes | length' <<<"$A")" -ge 1 ] && break; sleep 1 done
L=$(jq -r '.data.alerts.nodes[] | "alert #\(.alertID) \(.status): \(.summary)"' <<<"$A") case "$L" in *"StatusUnacknowledged: Checkout latency above 2s"*) echo "$L" ;; *) echo "the alert is not open: $A"; exit 1 ;; esac EOF_GOALERTservice: Web shop (integration key type generic) generic API: HTTP 204 alert #1 StatusUnacknowledged: Checkout latency above 2sThe alert now shows on the Alerts page and pages whoever the escalation policy targets.
From a monitoring system, send the same request to https://<your-goalert-host>/api/v2/generic/incoming with the integration key in an Authorization: Bearer <key> header. Copy a service's key from Services > (service) > Integration Keys. GoAlert also accepts the key as a token query parameter, but a header keeps it out of web server logs.
Add action=close with the same dedup value to close an alert automatically when the problem clears.
Deliver notifications (SMS, voice, email, Slack)
Notification providers are switched off until you configure them under Admin > Config with your own accounts:
- Twilio for SMS and voice calls
- SMTP for email
- Slack for Slack messages
- Webhook for outgoing webhooks
Their tokens are stored in the database, encrypted with this VM's data encryption key.
Twilio and Slack call back into GoAlert (delivery status, SMS replies, button presses). Those features need the VM to be reachable from them at its configured URL, with a certificate they trust. Put GoAlert under your own DNS name with a trusted certificate (Step 9) and allow their traffic on port 443. Outgoing webhooks and the generic API work without that.
Security checks
The environment file holds the database URL (a local socket, no password) and this VM's data encryption key. It is readable by root only, and systemd reads it before starting GoAlert as its own user. This command prints the variable names only:
sudo stat -c '%a %U:%G %n' /etc/goalert/goalert.env /root/goalert-credentials.txt /etc/ssl/goalert/goalert.key
sudo grep -oE '^[A-Z_]+=' /etc/goalert/goalert.env
sudo grep -E '^GOALERT_(DB_URL|LISTEN)=' /etc/goalert/goalert.env600 root:root /etc/goalert/goalert.env 600 root:root /root/goalert-credentials.txt 600 root:root /etc/ssl/goalert/goalert.key GOALERT_DB_URL= GOALERT_DATA_ENCRYPTION_KEY= GOALERT_LISTEN= GOALERT_DB_URL=postgres://goalert@/goalert?host=/var/run/postgresql GOALERT_LISTEN=127.0.0.1:8081PostgreSQL has no network listener and no role has a password. Local connections are allowed only for the matching operating system user:
sudo -u postgres psql -XtAc "SHOW listen_addresses" | sed 's/^$/(empty: no TCP listener)/'
sudo -u postgres psql -XtAc "SELECT count(*) || ' roles with a password' FROM pg_authid WHERE rolpassword IS NOT NULL"
sudo -u postgres psql -XtAc "SELECT type, database, user_name, auth_method FROM pg_hba_file_rules WHERE type = 'local'"(empty: no TCP listener) 0 roles with a password local|{all}|{all}|peer local|{replication}|{all}|peerGoAlert runs unprivileged in a sandbox with no capabilities. GoAlert, nginx and PostgreSQL cannot reach the Azure Instance Metadata Service (169.254.169.254) or the Azure wire server (168.63.129.16). This matters because GoAlert sends webhooks to addresses its users enter:
systemctl show goalert -p User -p ProtectSystem -p NoNewPrivileges -p CapabilityBoundingSet
systemctl show goalert nginx postgresql@16-main -p Id -p IPAddressDenyCapabilityBoundingSet= User=goalert ProtectSystem=strict NoNewPrivileges=yes IPAddressDeny=168.63.129.16/32 169.254.169.254/32 Id=goalert.service IPAddressDeny=168.63.129.16/32 169.254.169.254/32 Id=nginx.service IPAddressDeny=168.63.129.16/32 169.254.169.254/32 Id=postgresql@16-main.servicenginx passes GoAlert the configured host name and the real client address from the connection, never values a client sends. It also refuses any state-changing request or WebSocket upgrade whose Origin is another web site (HTTP 403):
grep -E 'proxy_set_header (Host|X-Forwarded-For|X-Real-IP)' /etc/nginx/sites-available/sac-goalertproxy_set_header Host $goalert_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr;The certificate was generated on this VM and names its own addresses. fwupd's firmware refresh is masked:
H=$(sudo grep '^GOALERT_URL=' /root/goalert-credentials.txt | sed -E 's#^GOALERT_URL=https://([^/]+)/.*#\1#')
echo | openssl s_client -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName for u in fwupd-refresh.timer fwupd-refresh.service; do
echo "$u: $(systemctl is-enabled "$u" 2>/dev/null)"; donesubject=CN = <vm-public-ip>, O = sac X509v3 Subject Alternative Name: IP Address:<vm-public-ip>, IP Address:<vm-private-ip>, IP Address:127.0.0.1, DNS:localhost fwupd-refresh.timer: masked fwupd-refresh.service: maskedThe session cookie GoAlert sets after sign-in is Secure, HttpOnly and SameSite=Strict. The sign-in form is refused when it is posted from any page other than the configured URL.
Azure configures SSH from the authentication type you chose when you created the VM (key or password). The image adds no SSH override.
Use your own DNS name and certificate
Point a DNS record at the VM's public IP, then set the name GoAlert is served as. This command mints a new self-signed certificate for the name and points GoAlert's public URL and every link it sends at that name. It also updates nginx and /root/goalert-credentials.txt, and keeps the name across reboots and deallocation:
sudo goalert-set-hostname <your-domain>To use a certificate from your own certificate authority (needed for Twilio and Slack callbacks), copy it over the generated pair and reload nginx. Running goalert-set-hostname again replaces it with a self-signed certificate:
sudo install -m 0644 <your-domain>.crt /etc/ssl/goalert/goalert.crt
sudo install -m 0600 <your-domain>.key /etc/ssl/goalert/goalert.key
sudo nginx -t && sudo systemctl reload nginxBackups, password and maintenance
Everything GoAlert knows lives in the goalert PostgreSQL database. Its stored secrets are encrypted with the key in /etc/goalert/goalert.env, so back up that file together with the database and keep both somewhere safe:
sudo -u postgres pg_dump -Fc goalert | sudo tee <backup-dir>/goalert-$(date +%F).dump >/dev/null
sudo install -m 0600 /etc/goalert/goalert.env <backup-dir>/goalert-$(date +%F).envTo change the admin password, sign in, open Users > admin, choose Edit Access and enter the current and the new password. Afterwards, update or delete GOALERT_ADMIN_PASSWORD in /root/goalert-credentials.txt so that it does not hold an old password.
GoAlert writes its logs to the journal:
sudo journalctl -u goalert --no-pager -n 3 -o cattime="2026-10-21T17:50:06Z" level=info msg="producer: Producer job counts" module=river num_completed_jobs=8 num_jobs_running=0 queue=status-managerGoAlert reads its startup flags from GOALERT_ environment variables. A flag such as --listen becomes GOALERT_LISTEN, and goalert --help lists the flags. Add them to /etc/goalert/goalert.env and run sudo systemctl restart goalert. Everything else is configured in the web interface under Admin.
Server components and build record
These are the components and where they live on the VM:
- GoAlert binary: /usr/local/bin/goalert
- Environment (database URL, encryption key): /etc/goalert/goalert.env (root only)
- Public URL: /etc/goalert/public-url.env (written by goalert-set-hostname)
- Service unit: /etc/systemd/system/goalert.service
- Database: PostgreSQL 16 cluster 16/main, /var/lib/postgresql/16/main, socket /var/run/postgresql
- nginx site: /etc/nginx/sites-available/sac-goalert
- Certificate and key: /etc/ssl/goalert/
- First-boot and per-boot host scripts: /usr/local/sbin/goalert-firstboot.sh, /usr/local/sbin/goalert-configure.sh
- Host name helper: /usr/local/sbin/goalert-set-hostname
- Credentials: /root/goalert-credentials.txt (mode 0600)
- Build record, licence and govulncheck reports: /usr/share/doc/goalert/
/usr/local/bin/goalert version sed -n '1,4p;/^binary sha256/p;/^govulncheck/,+2p' /usr/share/doc/goalert/BUILD-INFO.txtVersion: v0.35.0 GitCommit: db9829187a1c1f96a7c57112b74dba54442d7c6b (clean) BuildDate: 2026-09-15T21:24:20Z GoVersion: go1.27.3 (gc) Platform: linux/amd64 Migration: cm-private (#281) GoAlert v0.35.0 - sac source build (d-320) Licence: Apache-2.0 (LICENSE.md in this directory). The resulting go.mod and go.sum are in this directory. source https://github.com/target/goalert/archive/refs/tags/v0.35.0.tar.gz binary sha256 389f2c5db4de3a3c457ee2b9570601f2758ccfa18e92a8686a83caeb5ea68206 (reproducible) govulncheck v1.9.0: -mode=binary "No vulnerabilities found"; source mode 0 symbol and 0 package findings; module-level only GO-2026-5932 (golang.org/x/crypto/openpgp, no fixed version, not linked: go list -deps ./cmd/goalert has no x/crypto/openpgp package) (DB updated: 2026-10-21 00:27:29 +0000 UTC)Security notes and recommendations
About this build. Upstream GoAlert release binaries are compiled with older Go releases. Their standard library has known vulnerabilities reachable in GoAlert (TLS, X.509, HTTP and HTML template handling), all fixed in later Go releases. This image compiles the 0.35.0 source (tag commit db982918) with Go 1.27.3 and updated golang.org/x/net and golang.org/x/crypto modules. govulncheck finds no vulnerability in the shipped binary.
The only entry govulncheck lists is GO-2026-5932 (golang.org/x/crypto/openpgp, deprecated, no fixed version). It belongs to a required module that GoAlert never imports, so it is not in the binary at all.
The web interface is built from upstream's locked dependencies. The browser-side npm packages carry advisories, which are listed with their severities in /usr/share/doc/goalert/ui-npm-advisories.txt. They are denial-of-service and prototype-pollution issues reported for markdown-it, linkify-it, lodash, diff and mdast-util-to-hast, none critical. That code runs only in a signed-in user's browser. No JavaScript runtime runs on the server.
GoAlert 0.35.0 adds multi-acknowledge escalation steps, private contact methods, an alerts view per escalation policy and new admin hardening options. Every new behaviour is off until you enable it. One option is worth considering: Admin > Config > Webhook > Block Private Addresses stops outgoing webhooks to private, loopback and link-local addresses. This image already blocks the Azure metadata service for GoAlert.
Recommendations:
- Allow ports 22, 80 and 443 in the network security group from your own networks and monitoring systems only.
- Keep the admin password secret and give every person their own user. If you have an identity provider, switch on GitHub or OIDC sign-in under Admin > Config > Auth.
- Use your own DNS name and a certificate that your users and providers trust (Step 9).
- Keep the operating system patched. Unattended security upgrades are on, and
sudo apt-get update && sudo apt-get upgradeapplies everything else. - Back up the database and the environment file together (Step 10), or snapshot the OS disk.
Support and licensing
GoAlert is open source under the Apache License 2.0, with no per-user or per-CPU licence fee. The image price covers SecureAnyCloud's source build, hardening, first-boot setup and 24/7 support.
- Email: sales@secureanycloud.com
- Website: https://secureanycloud.com
Deploy on Azure
Launch GoAlert on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com
Overview
GoAlert is the open source on-call scheduling and alerting platform maintained by Target. It routes incidents to the right responder at the right time through on-call schedules, rotations and escalation policies. Once you connect a delivery provider, it delivers notifications by SMS, voice call, email and Slack.
This AMI runs GoAlert 0.34.1 as a single, self-contained Go binary. An unprivileged goalert system account runs it under a systemd service that starts it on boot and restarts it on failure. A local PostgreSQL 16 database on a dedicated data volume backs it.
GoAlert listens on the loopback address 127.0.0.1:8081 by design and is never exposed directly. nginx is installed as a reverse proxy on port 80 and forwards every request to GoAlert, including the WebSocket connections the UI uses for live updates. PostgreSQL is bound to loopback only.
What you get:
- GoAlert 0.34.1 (official upstream release binary, sha256 pinned), run by systemd as the unprivileged
goalertuser - The GoAlert web UI and API on
:80(nginx reverse proxy to127.0.0.1:8081) - A per-instance
adminaccount created on first boot and recorded in a root-only file - A per-instance data encryption key and a per-instance PostgreSQL password, both generated on first boot
- PostgreSQL 16 on a dedicated EBS data volume, bound to loopback, holding schedules, escalation policies, services and users
postgresql.service,goalert.serviceandnginx.serviceas enabled, active systemd units- An unauthenticated
/healthzendpoint for Elastic Load Balancing health checks - 24/7 SecureAnyCloud support
Connecting to your instance
Connect over SSH as the default login user for the operating system variant you launched. For Ubuntu 24.04, the SSH login user is ubuntu, so you connect with ssh ubuntu@<instance-public-ip>.
The security group must allow inbound 22/tcp from your management network and 80/tcp for the web UI.
Prerequisites
You need an AWS account, an EC2 key pair, and a VPC subnet in the target region.
m5.large (2 vCPU / 8 GiB RAM) is a sensible starting size. Choose a larger size for larger teams.
Configure the security group to allow inbound 22/tcp from your management network and 80/tcp for the web UI. GoAlert serves plain HTTP on port 80, so for production, terminate TLS in front of it with your own domain and set the Public URL accordingly (see Step 6).
Step 1 - Launch the instance
Subscribe to the listing in AWS Marketplace and launch it through the EC2 console or the AWS CLI. Select your key pair, an m5.large (or larger) instance type, and a security group that allows 22/tcp and 80/tcp inbound.
The dedicated PostgreSQL data volume is attached and mounted automatically.
Step 2 - Connect and confirm the services are running
nginx serves GoAlert on port 80 and reverse-proxies to the Go binary on 127.0.0.1:8081. The Go binary talks to PostgreSQL on 127.0.0.1:5432.
Connect over SSH, then confirm that all three services are active. Only nginx listens on a public address, while GoAlert and PostgreSQL are bound to loopback:
systemctl is-active postgresql goalert nginx goalert-firstboot
sudo ss -tlnp | grep -E ':80 |:8081 |:5432 'Expected output:
active active active active LISTEN 0 511 0.0.0.0:80 0.0.0.0:* LISTEN 0 200 127.0.0.1:5432 0.0.0.0:* LISTEN 0 4096 127.0.0.1:8081 0.0.0.0:* LISTEN 0 511 [::]:80 [::]:*Step 3 - Retrieve your admin password
On the first boot of every instance, goalert-firstboot.service generates a unique password for the admin account, a PostgreSQL password and a data encryption key. It runs the database migrations, then creates the single admin account with goalert add-user. It writes the admin username, password and dashboard URL into a root-only file. Read the file with:
sudo cat /root/goalert-credentials.txtThe image contains no default or shared credential. The admin account does not exist until first boot creates it with a password unique to this instance. Store the password somewhere safe.
Step 4 - Confirm the version and health endpoint
nginx serves an unauthenticated /healthz endpoint for load balancer health checks. Confirm the GoAlert and PostgreSQL versions, and confirm that the UI responds:
goalert version | head -4 psql --version curl -sI http://127.0.0.1/healthz | head -1 curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/Expected output:
Version: v0.34.1 GitCommit: 0918387e38650aaddd6a923d445ee992f64d6ab6 (clean) BuildDate: 2025-10-06T20:20:57Z GoVersion: go1.25.1 (gc) psql (PostgreSQL) 16.14 (Ubuntu 16.14-0ubuntu0.24.04.1) HTTP/1.1 200 OK 200Step 5 - Inspect the PostgreSQL backend on its dedicated volume
Everything GoAlert stores lives in the local goalert PostgreSQL database: schedules, rotations, escalation policies, services, users and alerts. The goalert role owns the database, and it is reachable only over loopback.
The database lives on a dedicated EBS data volume mounted at /var/lib/postgresql. The volume is separate from the OS disk and can be resized and snapshotted independently. The image ships with the schema migrated and exactly one account, the per-instance admin:
findmnt /var/lib/postgresql
sudo -u postgres psql -tAc "SHOW data_directory"
sudo -u postgres psql -l | grep goalert
sudo -u postgres psql -d goalert -tAc "SELECT count(*) FROM users"Expected output:
TARGET SOURCE FSTYPE OPTIONS /var/lib/postgresql /dev/nvme1n1 ext4 rw,relatime /var/lib/postgresql/16/main goalert | goalert | UTF8 | libc | C.UTF-8 | C.UTF-8 | 1Step 6 - Sign in to the web UI
Browse to http://<instance-public-ip>/. Enter admin and the password from Step 3.
When you sign in, GoAlert validates that your browser's address matches its configured Public URL. On this image, goalert-configure.service sets the Public URL to the instance's detected public IP on every boot (resolved through EC2 IMDSv2), so signing in over the instance public IP works out of the box.
If you reach GoAlert through a different hostname (a DNS name, a load balancer or your own TLS proxy), set the correct URL once. Edit GOALERT_PUBLIC_URL in /etc/goalert/goalert.env and run sudo systemctl restart goalert, or set it from the Admin page after you sign in.
Step 7 - Build your on-call schedules
Open Schedules from the navigation and click Create Schedule. Give the schedule a name and time zone, then add rotations and assign users to define who is on call and when.
Schedules are the foundation of GoAlert. Escalation policies notify whoever a schedule says is on call at the moment an alert fires.
Step 8 - Define escalation policies
Open Escalation Policies and click Create Escalation Policy. An escalation policy is an ordered set of steps: notify this schedule, wait N minutes, then notify the next. It has an optional repeat count, so an unacknowledged alert escalates automatically until someone responds.
Step 9 - Register your services and receive alerts
Open Services and click Create Service, then attach the service to an escalation policy. A service represents something you want alerts for, such as an API, a website or a job.
Each service gets integration keys: a generic API endpoint, plus Grafana, Prometheus Alertmanager, email and other formats. Your monitoring tools call these keys to raise an alert, and GoAlert routes it through the service's escalation policy. Active alerts appear in the UI, where responders acknowledge and close them.
Step 10 - Enable alert delivery (SMS, voice, email, Slack)
The image ships the fully functional UI, schedules, escalation policies and services. To deliver notifications, connect a provider from the Admin -> Config page after you sign in:
- Twilio for SMS and voice calls (Account SID, Auth Token, From Number)
- SMTP for email notifications to responders
- Slack to notify channels as part of an escalation policy
- Mailgun or the built-in SMTP ingress to create alerts from inbound email
Switch on and configure each provider in its own section of the Admin Config page. See the GoAlert documentation for provider-specific setup.
Maintenance
- Back up your data. Everything GoAlert stores lives in the local
goalertPostgreSQL database on the dedicated data volume. Snapshot the volume with EBS snapshots or AWS Backup, or usepg_dump goalertfor logical backups. - Resize storage. The database volume is independent of the OS disk. As your alert history grows, grow the volume with an EBS volume modification and
resize2fs. - Public URL. The Public URL is set to the detected public IP on each boot. For a stable hostname or HTTPS, set
GOALERT_PUBLIC_URLin/etc/goalert/goalert.envand runsudo systemctl restart goalert. - Security updates. Keep the OS patched with
sudo apt-get update && sudo apt-get upgrade, and reboot when a new kernel is installed. - TLS. For production, front GoAlert with your own domain and a TLS-terminating reverse proxy or an Application Load Balancer, and set the Public URL to the
https://address. - Add responders. Open Users to manage accounts. For larger teams, enable GitHub or OIDC single sign-on from Admin -> Config.
Support
SecureAnyCloud provides 24/7/365 expert technical support for this image. Contact sales@secureanycloud.com.
GoAlert is licensed under the Apache License 2.0. This image is provided by SecureAnyCloud, and additional charges apply for build, maintenance and 24/7 support.
Deploy on AWS
Launch GoAlert on AWS with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com