Streaming & Messaging Azure

Sequin on Ubuntu 24.04 on Azure User Guide

| Product: Sequin on Ubuntu 24.04 LTS on Azure

Overview

This guide covers the deployment and configuration of Sequin on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. Sequin is the open source change data capture platform for PostgreSQL: it attaches to a logical replication slot, reads your database's write ahead log, and turns every insert, update and delete into a durable, ordered stream of change events delivered to the destination of your choice. Instead of writing triggers, polling tables or building a bespoke outbox, you point Sequin at a database and a sink, and it handles replication, ordering, batching, retries, backfills and delivery observability.

The cloudimg image ships the free and open source, MIT licensed Sequin server, run the officially supported way as the upstream container pinned by image digest, alongside a bundled PostgreSQL 16 started with wal_level=logical and a bundled Redis 7. All three images are pinned by digest and captured into the VM, so your instance starts in seconds without pulling anything. Because a CDC platform holds credentials for your production databases, nothing ships with a known secret: upstream Sequin's default admin@sequinstream.com login and open self signup are both disabled, and a unique console credential, admin password, Management API token, database password, Redis password and TLS certificate are generated for each VM on first boot, before anything is reachable. Backed by 24/7 cloudimg support.

Sequin is a trademark of its respective owner. This image is produced by cloudimg and is not affiliated with, endorsed by, or sponsored by Sequin Labs, Inc. It ships the free and open source MIT licensed self hosted software, unmodified.

The docker, sequin firstboot, sequin, sequin postboot and nginx services all active, the three pinned containers running, and the listeners showing nginx on ports 80 and 443 with the Sequin application bound to loopback only on 127.0.0.1:7376

What is included:

  • Sequin v0.14.6 (the MIT licensed Sequin change data capture server and web console), pinned by image digest
  • A bundled PostgreSQL 16 started with wal_level=logical, pinned by image digest, reachable only inside a private Docker network (never published to a host port)
  • A bundled Redis 7 queue and cache, pinned by image digest, password protected and reachable only inside the private Docker network
  • Docker Engine with the Sequin application published to the loopback interface only, fronted by nginx with TLS on port 443
  • sequin.service, sequin-firstboot.service, sequin-postboot.service and nginx.service as systemd units, enabled and active on boot
  • A unique console credential, admin password, Management API token, PostgreSQL password, Redis password and self signed TLS certificate generated per VM on first boot, never baked into the image
  • Upstream Sequin's default user and open self signup both disabled, so there is no known login to change
  • A working demo change data capture pipeline out of the box, so you can see real captured changes within seconds of first boot
  • A clean, empty database on first boot: no default login, no shipped secret, no prior data
  • Ubuntu 24.04 LTS base with latest security patches applied at build time
  • Azure Linux Agent for seamless cloud integration and SSH key injection

Prerequisites

  • Active Azure subscription, SSH public key, VNet + subnet in target region
  • Subscription to the Sequin listing on Azure Marketplace

Recommended virtual machine size: Standard_D2s_v3 (2 vCPU, 8 GB RAM). Sequin runs on the Erlang virtual machine and this image co-locates the server, PostgreSQL and Redis on one host, so 8 GB is the sensible production starting point. Standard_B2s (2 vCPU, 4 GB) works for evaluation and light workloads. For higher throughput or large backfills use Standard_D4s_v3 or larger. NSG inbound: allow 22/tcp from your management network and 443/tcp for the console and Management API from the networks that use them.

Step 1: Deploy from the Azure Portal

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Sequin by cloudimg, and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22) and HTTPS (443). Then Review + create and Create.

Step 2: Deploy from the Azure CLI

RG="sequin-prod"; LOCATION="eastus"; VM_NAME="sequin-01"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/sequin-ubuntu-24-04/versions/<version>"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"
az group create --name "$RG" --location "$LOCATION"
az network vnet create -g "$RG" --name sequin-vnet --address-prefix 10.100.0.0/16 --subnet-name sequin-subnet --subnet-prefix 10.100.1.0/24
az network nsg create -g "$RG" --name sequin-nsg
az network nsg rule create -g "$RG" --nsg-name sequin-nsg --name allow-ssh --priority 100 \
  --source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 22 --access Allow --protocol Tcp
az network nsg rule create -g "$RG" --nsg-name sequin-nsg --name allow-https --priority 110 \
  --source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 443 --access Allow --protocol Tcp
az vm create -g "$RG" --name "$VM_NAME" --image "$GALLERY_IMAGE_ID" \
  --size Standard_D2s_v3 --storage-sku StandardSSD_LRS \
  --admin-username azureuser --ssh-key-values "$SSH_KEY" \
  --vnet-name sequin-vnet --subnet sequin-subnet --nsg sequin-nsg --public-ip-sku Standard

Step 3: Connect to your VM

ssh azureuser@<vm-ip>

Step 4: Confirm the services are running

The stack runs as three containers (Sequin, PostgreSQL and Redis) under one sequin.service, fronted by nginx. Confirm the services are active, see the containers, and check which ports are bound. nginx listens on :80 (which redirects to HTTPS) and :443, the Sequin application is published to 127.0.0.1:7376 (loopback only), and PostgreSQL and Redis have no host port at all:

sudo systemctl is-active docker sequin nginx
sudo docker compose --env-file /etc/sequin/sequin.env -f /etc/sequin/compose.yaml ps
sudo ss -tln | grep -E ':80 |:443 |:7376'

You will see all three services report active, the three containers running, and nginx bound on :80 and :443 with the Sequin application bound only on 127.0.0.1:7376. PostgreSQL (5432) and Redis (6379) never appear on a host port because they are reachable only inside the private Docker network. This is deliberate: the only way in from the network is the authenticated nginx front door.

The systemd services all active, the three pinned containers running under docker compose, and ss showing nginx on ports 80 and 443 with Sequin on loopback 127.0.0.1:7376 and PostgreSQL and Redis on no host port

Step 5: Read the per instance credentials

A unique console credential, admin password, Management API token, database password and Redis password were generated for this VM on the first boot, before anything was reachable, and written to a root only file. Read them:

sudo cat /root/sequin-credentials.txt

The file (mode 0600 root:root) holds CONSOLE_USER and CONSOLE_PASSWORD (the nginx credential that gates the web console), ADMIN_EMAIL and ADMIN_PASSWORD (Sequin's own login), the API_TOKEN you authenticate the Management API with, POSTGRES_PASSWORD, REDIS_PASSWORD, and SEQUIN_URL (the address to reach this VM on). None of these ship in the image; every value is unique to this VM, so no two instances share a credential and there is no default login to change.

The sequin-credentials.txt file at mode 0600 root root with the per VM console credential, admin password, Management API token, PostgreSQL password and Redis password all masked, and a note that they are generated uniquely on first boot

Step 6: Verify the security model

The health endpoint is public so load balancers can probe it, but the console requires the per instance nginx credential and the Management API requires the per instance bearer token. Confirm all three behaviours:

SEQUIN_TOKEN=$(sudo grep '^API_TOKEN=' /root/sequin-credentials.txt | cut -d= -f2-)
SEQUIN_USER=$(sudo grep '^CONSOLE_USER=' /root/sequin-credentials.txt | cut -d= -f2-)
SEQUIN_PASS=$(sudo grep '^CONSOLE_PASSWORD=' /root/sequin-credentials.txt | cut -d= -f2-)
curl -sk -o /dev/null -w 'health   (no auth):        %{http_code}\n' https://localhost/health
curl -sk -o /dev/null -w 'console  (no credential):  %{http_code}\n' https://localhost/
curl -sk -o /dev/null -w 'console  (per VM cred):    %{http_code}\n' -u "$SEQUIN_USER:$SEQUIN_PASS" https://localhost/
curl -sk -o /dev/null -w 'API      (no token):       %{http_code}\n' https://localhost/api/sinks
curl -sk -o /dev/null -w 'API      (per VM token):   %{http_code}\n' -H "Authorization: Bearer $SEQUIN_TOKEN" https://localhost/api/sinks

The health check returns 200 with no authentication. The console returns 401 with no credential and 302 (a redirect to the Sequin login) once the per instance credential is supplied — so reaching the login page at all requires a secret. The Management API returns 401 with no token and 200 with the per instance token.

Note that the Management API is not behind the nginx credential. HTTP Basic authentication and bearer tokens both use the Authorization header, so a client cannot present both at once; the API is therefore gated by Sequin's own per instance API token instead. Every path is authenticated by a per instance secret except /health.

curl showing the health endpoint returning HTTP 200 with no auth, the console rejected with HTTP 401 for no credential and a wrong credential, the Management API rejected with HTTP 401 for no token and a wrong token, and the feature flags showing the upstream default user and self signup both disabled

Step 7: Open the web console

Browse to https://<vm-ip>/. Your browser will warn about the self signed certificate that was generated for this VM on first boot — accept it, or replace the certificate with your own as described in Step 11.

You will be prompted twice, by design. First the nginx front door asks for CONSOLE_USER and CONSOLE_PASSWORD. Then Sequin's own login page asks for ADMIN_EMAIL and ADMIN_PASSWORD. Both pairs come from /root/sequin-credentials.txt.

The Sequin login page served over TLS, asking for an email and password, with the Sequin branding and version v0.14.6 shown

Once signed in you land on the Sinks page. The image ships a working demo pipeline called demo-redis-stream, already Healthy and reading from the bundled demo-source database:

The Sequin console Sinks page showing the bundled demo-redis-stream sink with a green Healthy status, reading from the demo-source database, with the navigation sidebar listing Databases, Change Retention, Functions and HTTP Endpoints

The Databases page shows the bundled PostgreSQL registered as a change data capture source, with its logical replication slot and publication:

The Sequin console Databases page showing the demo-source PostgreSQL database registered as a change data capture source with its replication slot and publication

Step 8: Watch real changes being captured

The image ships a demo source database, sequin_demo, whose public.records table is published to the replication slot and streamed to the Redis stream sequin.records. Write a row and watch it arrive:

sudo docker compose --env-file /etc/sequin/sequin.env -f /etc/sequin/compose.yaml exec -T sequin_postgres \
  psql -U sequin -d sequin_demo -c "insert into public.records (name) values ('order-4711');"
sleep 8
REDIS_PASS=$(sudo grep '^REDIS_PASSWORD=' /root/sequin-credentials.txt | cut -d= -f2-)
sudo docker compose --env-file /etc/sequin/sequin.env -f /etc/sequin/compose.yaml exec -T sequin_redis \
  redis-cli --no-auth-warning -a "$REDIS_PASS" XREVRANGE sequin.records + - COUNT 1

The Redis stream entry contains the full change event: the record (your new row), the action (insert), the changes (for updates, the previous values) and metadata identifying the sink, database and table. That is the whole point of Sequin — your row change became a durable, ordered message in a stream, with no trigger and no polling.

Now prove the pipeline is genuinely filtering rather than forwarding everything. public.ignored_records exists in the same database but is deliberately not in the publication, so writes to it must never reach the sink:

REDIS_PASS=$(sudo grep '^REDIS_PASSWORD=' /root/sequin-credentials.txt | cut -d= -f2-)
BEFORE=$(sudo docker compose --env-file /etc/sequin/sequin.env -f /etc/sequin/compose.yaml exec -T sequin_redis \
  redis-cli --no-auth-warning -a "$REDIS_PASS" XLEN sequin.records | tr -d '[:space:]')
sudo docker compose --env-file /etc/sequin/sequin.env -f /etc/sequin/compose.yaml exec -T sequin_postgres \
  psql -U sequin -d sequin_demo -c "insert into public.ignored_records (name) values ('never-streamed');"
sleep 10
AFTER=$(sudo docker compose --env-file /etc/sequin/sequin.env -f /etc/sequin/compose.yaml exec -T sequin_redis \
  redis-cli --no-auth-warning -a "$REDIS_PASS" XLEN sequin.records | tr -d '[:space:]')
echo "stream length before=$BEFORE after=$AFTER (unpublished writes are correctly not delivered)"

The stream length is unchanged: the write to the unpublished table was correctly not delivered.

Terminal output showing wal_level logical, the sequin_pub publication and an active sequin_slot replication slot, an insert into public.records, the captured change event delivered into the Redis stream with its record, action and metadata, and the negative control showing the stream length unchanged after writing to an unpublished table

The console shows the same thing. Open the sink and its Messages tab to see the delivered change events, throughput and health:

The Sequin console showing the demo-redis-stream sink detail page with its Redis Stream destination configuration, health status and delivery metrics

The Sequin console Messages tab for the demo-redis-stream sink, listing the change events that have been captured from public.records and delivered to the Redis stream

Step 9: Query the Management API

Everything in the console is also available over the Management API, authenticated with the per instance bearer token. List the configured sinks and databases:

SEQUIN_TOKEN=$(sudo grep '^API_TOKEN=' /root/sequin-credentials.txt | cut -d= -f2-)
curl -sk -H "Authorization: Bearer $SEQUIN_TOKEN" https://localhost/api/sinks | head -c 400; echo
curl -sk -H "Authorization: Bearer $SEQUIN_TOKEN" https://localhost/api/postgres_databases | head -c 400; echo

Use the same token from your own machine by replacing localhost with your VM address, for example https://<vm-ip>/api/sinks. Treat the token as a production secret: it grants full control of every pipeline on this instance.

Step 10: Connect your own database

The demo pipeline exists so the image is useful immediately, but the real workflow is pointing Sequin at your own PostgreSQL. Sequin requires the source database to have wal_level = logical, plus a replication slot and a publication. On your own database, run the following as a superuser (this is a template, not something to run on this VM):

ALTER SYSTEM SET wal_level = logical;
-- restart PostgreSQL for wal_level to take effect
CREATE PUBLICATION sequin_pub FOR TABLE public.orders, public.customers;
SELECT pg_create_logical_replication_slot('sequin_slot', 'pgoutput');

Then in the console choose Databases, Connect Database, and supply the host, port, database, user and password along with the slot and publication names. Once the database is connected, choose Sinks, Create Sink, pick a destination — Kafka, SQS, SNS, Kinesis, NATS, RabbitMQ, Redis Streams, GCP Pub/Sub, Azure Event Hubs, Elasticsearch, Typesense, Meilisearch, an HTTP webhook, or Sequin's own pull API — and select the tables and actions to stream. You can also run an initial backfill so the sink starts from the current contents of the table rather than only future changes.

Pipelines can equally be declared as configuration. Sequin reads a YAML document describing accounts, databases and sinks, which is how this image provisions its own demo pipeline:

databases:
  - name: "production"
    hostname: "db.internal"
    port: 5432
    database: "app"
    username: "sequin"
    password: "..."
    slot:
      name: "sequin_slot"
    publication:
      name: "sequin_pub"

sinks:
  - name: "orders-to-kafka"
    database: "production"
    source:
      include_tables:
        - "public.orders"
    actions: [insert, update, delete]
    destination:
      type: "kafka"
      hosts: "kafka.internal:9092"
      topic: "orders"

Step 11: Use your own certificate and domain (production)

The image generates a self signed TLS certificate for the VM's own IP on first boot, so the console is encrypted from the very first request with no configuration. For production, point a DNS name at the VM and install a real certificate. Replace /etc/sequin/tls/sequin.crt and /etc/sequin/tls/sequin.key with your certificate and private key (keep the key 0600 root:root), then reload nginx. Also set SERVER_HOST in /etc/sequin/sequin.env to your DNS name so the console generates correct absolute links, and restart the stack.

You can rotate the console credential at any time with htpasswd -B /etc/sequin/console.htpasswd cloudimg, and change the Sequin admin password from the console under account settings.

Step 12: Server components

Component Detail
Sequin v0.14.6, upstream container pinned by digest, published to 127.0.0.1:7376
PostgreSQL 16 (alpine), pinned by digest, wal_level=logical, private Docker network only
Redis 7 (alpine), pinned by digest, password protected, private Docker network only
Front door nginx, TLS on 443, 80 redirects, per VM bcrypt credential on the console
Config /etc/sequin/compose.yaml, per VM secrets in /etc/sequin/sequin.env (0600)
Credentials /root/sequin-credentials.txt (0600 root:root)
Data Docker named volumes sequin_pgdata and sequin_redisdata
Licence MIT

Step 13: Managing the Sequin service

sudo systemctl status sequin --no-pager | head -20
sudo docker compose --env-file /etc/sequin/sequin.env -f /etc/sequin/compose.yaml ps
sudo docker compose --env-file /etc/sequin/sequin.env -f /etc/sequin/compose.yaml logs --tail 20 sequin

Use sudo systemctl restart sequin to restart the whole stack (the unit runs docker compose up -d --wait), and sudo systemctl stop sequin to bring it down. The named volumes persist across restarts, so your pipelines, connected databases and replication progress survive.

Step 14: Security recommendations

  • Restrict 443/tcp in the NSG to the networks that genuinely need the console and Management API, and 22/tcp to your management network only
  • Replace the self signed certificate with a certificate for a real DNS name (Step 11)
  • Treat /root/sequin-credentials.txt as a production secret; copy the values into your secret manager and consider removing the file afterwards
  • Rotate the Management API token from the console if it is ever shared, and rotate the console credential with htpasswd
  • Give Sequin a dedicated PostgreSQL role on each source database with REPLICATION and read access to the published tables only, never a superuser
  • Keep the VM patched; unattended security upgrades are enabled on this image

Step 15: Support and Licensing

Sequin is licensed under the MIT License. This image bundles the unmodified upstream software and is produced by cloudimg, which is not affiliated with or endorsed by Sequin Labs, Inc.

Deploy on Azure

Find Sequin on Ubuntu 24.04 LTS by cloudimg on the Azure Marketplace.

Need Help?

cloudimg provides 24/7 support for this image. Contact us at cloudimg.co.uk.