Baby Buddy 2.11.0 on AWS User Guide
Overview
This guide covers launching and verifying Baby Buddy 2.11.0 on Ubuntu 24.04 from cloudimg on Amazon EC2. It is one version of the cloudimg Hardened Actual Budget & Self-Hosted Personal Apps AMIs listing on AWS Marketplace; you pick it as the software version when you subscribe and launch. The other personal apps in the listing are described on the listing's guide hub.
Baby Buddy is a free, self-hosted web application (BSD-2-Clause) that helps parents and caregivers track a baby's feedings, sleep, diaper changes, tummy time, pumping, medication, temperature and growth. It shows the latest of everything on a dashboard of cards, keeps a chronological timeline you can correct, draws reports and charts for every kind of entry, and offers a REST API for apps and home-automation tools. Several caregivers can share one instance, each with their own account.
What is on the image:
- Baby Buddy 2.11.0 from the official v2.11.0 release archive (pinned by SHA-256), with its prebuilt web assets, in a Python 3.12 virtual environment under
/opt/babybuddy/venv - All 48 Python packages installed from a cloudimg lock that pins every version and file hash (
pip --require-hashes), with pip 26.3. The lock takes Pillow 13.0.0 and Django REST framework 3.18.3, newer than upstream's pins, so no package carries a known advisory; Baby Buddy's own test suite passes on them - Baby Buddy (gunicorn) on
127.0.0.1:8000only, behind nginx serving HTTPS on port 443 with a certificate created on your instance; port 80 redirects to HTTPS - A per-instance administrator account created on first boot; the upstream
admin/adminaccount is never created - The SQLite database, Django's secret key and uploaded photos in
/var/lib/babybuddy(mode 0700, owned by thebabybuddyservice account) babybuddy-set-hostname, which serves Baby Buddy under your own DNS name,babybuddy-reset-password, a root-only recovery tool, andbabybuddy-selftest, an end-to-end check
Default user: ubuntu · Architecture: x86_64 · Root volume: 12 GiB gp3
Security posture
- No default account. Upstream Baby Buddy's first migration creates an administrator
adminwith the passwordadmin. This image replaces that step: first boot creates the administratoradminwith a 32-character password generated on your instance, so the upstream account never exists. The password reaches the provisioning step through a temporary root-only file, never on a command line or in a logged environment, and is written to a root-only file. - Per-instance secrets. Django's secret key, the administrator password and the TLS key are generated on your instance. The image ships without a database: first boot creates it, so no two instances share a secret or a session.
- Production settings. Django's debug mode is off,
ALLOWED_HOSTSand the CSRF trusted origins name only your instance's own host, session and CSRF cookies areSecure, and the upstreamresetandfakemanagement commands, which wipe or fill the database, are refused. - Fails closed. Baby Buddy starts only after first boot has written its key, database and administrator, and nginx only after first boot has finished. If first boot cannot finish, nothing is served.
- Only nginx faces the network. Exactly ports 22, 80 and 443 listen beyond loopback. Baby Buddy runs as the unprivileged
babybuddyuser under a systemd sandbox (read-only system, no home directories, no new privileges, no capabilities, its own/tmp), and cannot read the credentials file or the TLS key. - HTTPS from first boot. nginx serves TLS 1.2 and 1.3 with a certificate created on your instance (an end-entity certificate, not a CA; TLS 1.1 is refused) and sends
Strict-Transport-Security. - The host name comes from configuration, never from a request. First boot reads the instance's public DNS name (else its public or private IPv4 address) from EC2 instance metadata (IMDSv2) and pins nginx and Django to it. Requests for any other host name get their connection closed; requests to the instance's IP addresses are redirected to that name. nginx drops client-sent
X-Forwarded-Hostand refuses state-changing requests that come from other sites. - Client address from the connection. nginx replaces
X-Forwarded-ForandX-Real-IPwith the real peer address and dropsForwarded, so the sign-in lockout (django-axes: five failed sign-ins lock the address out) cannot be dodged or turned on someone else with a forged header. At build time a failed sign-in carrying forged forwarding headers was recorded with the real peer address. - The app cannot read instance metadata. Baby Buddy, the first-boot unit, nginx and every helper's app step are blocked from the instance metadata service (169.254.169.254, fd00:ec2::254) by systemd
IPAddressDeny; only the small unit that looks up the host name at first boot may use it. - Photos are private. Child photos are stored mode 0600 and served only to a signed-in session; a signed-out request is sent to the sign-in page.
- Root-only recovery. If the administrator is locked out, deactivated, expired or deleted,
sudo babybuddy-reset-passwordrestores it with a new generated password (Step 9). The helpers refuse symbolic links planted where they write, and--helpor an unknown argument changes nothing. - Hardened SSH and system. SSH allows keys only, no root login and no X11 forwarding. The AMI requires IMDSv2. SSH host keys and the machine ID are generated fresh on every instance.
Prerequisites
- An AWS account and permission to launch EC2 instances
- An EC2 key pair in the Region you launch in
Recommended instance type: m5.large. Baby Buddy itself needs little (two gunicorn workers use around 150 MiB of memory); a burstable t3.small is enough for a single family.
Connecting to your instance
| Version | Operating system | SSH user |
|---|---|---|
| Baby Buddy 2.11.0 on Ubuntu 24.04 | Ubuntu 24.04 LTS | ubuntu |
Step 1: Launch the instance
Option A: AWS Marketplace console
- Open the cloudimg Hardened Actual Budget & Self-Hosted Personal Apps AMIs listing on AWS Marketplace and choose View purchase options, then Subscribe. One subscription covers every version in the listing.
- Once the subscription is active, choose Launch your software.
- Choose One-click launch from AWS Marketplace (or Launch from EC2 Console for the full launch wizard). In Version, choose Baby Buddy 2.11.0 on Ubuntu 24.04, then choose your Region.
- Pick an instance type (
m5.largerecommended; the software charge is $0.04 per vCPU per hour) and your key pair. - Security group: the listing's default group opens SSH (22), HTTP (80) and HTTPS (443). Restrict SSH to your own IP address, and restrict 80 and 443 to the networks your family and caregivers use.
- Choose your VPC and subnet, leave the metadata defaults (the image already requires IMDSv2), then choose Launch.
Option B: AWS CLI
Run this on your own workstation. Replace the AMI ID with the one shown for your Region on the Marketplace launch page, and the VPC, subnet, key pair and CIDRs with your own:
SG=$(aws ec2 create-security-group --group-name baby-buddy \
--description "Baby Buddy" --vpc-id vpc-0123456789abcdef0 \
--query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 22 --cidr <your-mgmt-cidr>
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 443 --cidr <your-mgmt-cidr>
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 80 --cidr <your-mgmt-cidr>
aws ec2 run-instances \
--image-id ami-0123456789abcdef0 \
--instance-type m5.large \
--key-name my-key-pair \
--security-group-ids "$SG" \
--subnet-id subnet-0123456789abcdef0 \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=baby-buddy}]'
You do not need --metadata-options: the AMI already makes IMDSv2 required. Associate an Elastic IP if you want the address to survive a stop and start, then run babybuddy-set-hostname for its name (Step 8).
Step 2: Connect and find your login
Connect as ubuntu with the key pair you chose:
ssh -i ~/.ssh/my-key-pair.pem ubuntu@<public-ip>
First boot takes under a minute after the instance starts. It sets the name Baby Buddy is served as (the instance's public DNS name when it has one), creates the TLS certificate, Django's secret key, the database and the administrator, and writes /var/lib/cloudimg/babybuddy-credentials.txt (mode 0600, root). These are the entries in that file (the password is not shown here):
sudo grep -E '^(BABYBUDDY_URL|ADMIN_USERNAME)=' /var/lib/cloudimg/babybuddy-credentials.txt
sudo grep -oE '^[A-Z_]+=' /var/lib/cloudimg/babybuddy-credentials.txt
sudo stat -c '%a %U:%G %n' /var/lib/cloudimg/babybuddy-credentials.txt
BABYBUDDY_URL=https://ec2-3-89-65-185.compute-1.amazonaws.com/
ADMIN_USERNAME=admin
BABYBUDDY_URL=
ADMIN_USERNAME=
ADMIN_PASSWORD=
600 root:root /var/lib/cloudimg/babybuddy-credentials.txt
| Value | What it is for |
|---|---|
BABYBUDDY_URL |
The address of this instance's Baby Buddy |
ADMIN_USERNAME |
The administrator's user name (admin) |
ADMIN_PASSWORD |
Administrator password, generated on this instance |
To read the password, run sudo cat /var/lib/cloudimg/babybuddy-credentials.txt in your own session and store it in your password manager. Baby Buddy keeps only a PBKDF2 hash of it.
Step 3: Confirm Baby Buddy is running
The two first-boot units have finished, Baby Buddy and nginx are active, and only sshd and nginx listen beyond loopback (gunicorn is on 127.0.0.1:8000, the resolver on 127.0.0.53):
systemctl is-active babybuddy-host.service babybuddy-firstboot.service babybuddy.service nginx.service
sudo ss -Htlnp | awk '{print $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort -u
cat /opt/babybuddy/VERSION
active
active
active
active
0.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:8000 users:(("gunicorn"),("gunicorn"),("gunicorn"))
127.0.0.53%lo:53 users:(("systemd-resolve"))
127.0.0.54:53 users:(("systemd-resolve"))
[::]:22 users:(("sshd"),("systemd"))
[::]:443 users:(("nginx"),("nginx"),("nginx"))
[::]:80 users:(("nginx"),("nginx"),("nginx"))
babybuddy_version=2.11.0
babybuddy_source=https://github.com/babybuddy/babybuddy/archive/refs/tags/v2.11.0.tar.gz (sha256 f826e035d88e460d0ca2e38b7c311a094de300028e401c933a223f809d38931b, commit 4c14f7428f24a725710d8deb03d5f28105e4da4e)
python=3.12.3
pip=26.3
requirements_sha256=4246cbda66717df0a7477fc0f6d9afe52d2d0917876c6d3b59d5342f308ffda3
Step 4: Prove the login and the web defaults
The sign-in page and the health check answer, the dashboard and the API refuse a signed-out request, plain HTTP redirects to HTTPS, an unknown Host gets no answer and TLS 1.1 is not offered. The last command is the image's own round-trip check: it signs in with this instance's administrator password, proves a wrong password is refused, reads the API with the session, proves a signed-out client is refused, and signs out:
H=$(sed -n 's/^set \$bb_host "\(.*\)";$/\1/p' /etc/nginx/babybuddy-hostvar.conf)
C=(--connect-to ::127.0.0.1:)
curl -sk "${C[@]}" -o /dev/null -w 'sign-in page -> %{http_code}\n' "https://$H/login/"
curl -sk "${C[@]}" -o /dev/null -w 'health check -> %{http_code}\n' "https://$H/healthz"
curl -sk "${C[@]}" -o /dev/null -w 'dashboard, signed out -> %{http_code}\n' "https://$H/dashboard/"
curl -sk "${C[@]}" -o /dev/null -w 'API, signed out -> %{http_code}\n' "https://$H/api/children/"
curl -s "${C[@]}" -o /dev/null -w 'http -> %{http_code}\n' "http://$H/"
curl -sk -o /dev/null -w 'unknown Host -> %{http_code}\n' --resolve evil.example:443:127.0.0.1 https://evil.example/ || true
curl -sk "${C[@]}" -o /dev/null -w 'TLS 1.1 -> %{http_code}\n' --tlsv1.1 --tls-max 1.1 "https://$H/" || true
sudo babybuddy-selftest
sign-in page -> 200
health check -> 200
dashboard, signed out -> 302
API, signed out -> 403
http -> 301
unknown Host -> 000
TLS 1.1 -> 000
round-trip OK: admin signed in over https://ec2-3-89-65-185.compute-1.amazonaws.com/ (wrong password refused), the API served to the session, anonymous API 403 and browser sent to /login/, sign-out ended the session (403)
--connect-to ::127.0.0.1: sends each request to this instance while keeping the real host name in the request, exactly as a browser would send it. 000 means the server closed the connection without an answer. The 302 sends a signed-out browser to the sign-in page. /healthz is the only page that answers without a sign-in, so you can point a load balancer health check at it.
Step 5: Sign in to Baby Buddy
Browse to BABYBUDDY_URL (https://<public-dns-name>/). The certificate is self-signed, so your browser warns once; accept it for this host, or install your own certificate (Step 8). Sign in with ADMIN_USERNAME and ADMIN_PASSWORD.

Step 6: Add your child and start tracking
The image ships with no children and no entries. Open Children, then Add Child, and enter a first name, last name and birth date (a photo is optional). Then record activities from the Activities and Measurements menus, or start a Timer when a feeding or nap begins and turn it into an entry when it ends.
Once a few entries exist, the child's dashboard shows the latest of everything at a glance: the last feeding and its method and amount, the last diaper change, the last sleep and today's naps, tummy time, timers and rolling statistics. The screenshots below were taken on a test instance with a few days of sample entries, which were removed before the image was made.

Timeline lists every entry in order with how long ago it happened; use Edit on any entry to correct it.

Step 7: Reports
Open the child and choose the chart icon (or Reports) for charts of feeding amounts, durations, patterns and methods, sleep patterns and totals, diaper change types, amounts and intervals, tummy time, pumping, medication, temperature, and growth (weight, height, head circumference and BMI, with weight and height percentile bands).

Step 8: Serve Baby Buddy under your own DNS name
Point a DNS record (for example baby.example.com) at the instance's public or Elastic IP address, then tell the image to serve that name. It creates a new certificate for the name, pins nginx, Django's allowed hosts and the CSRF trusted origins to it, updates BABYBUDDY_URL in the credentials file, and keeps the name across reboots:
sudo babybuddy-set-hostname baby.example.com
For a trusted certificate, obtain one for that name (for example with certbot's certonly mode using its own temporary web server, or from your organisation's CA), copy the certificate chain and key over /etc/ssl/babybuddy/babybuddy.crt and /etc/ssl/babybuddy/babybuddy.key (key mode 0600, root), and run sudo systemctl reload nginx.
Step 9: Accounts and recovery
- Administrator password: change it from the admin menu (top right), then Password. The credentials file is not updated; remove the old password from it (
sudo nano /var/lib/cloudimg/babybuddy-credentials.txt) once you have stored the new one. - Other caregivers: add them from the admin menu, then Users. Give staff or superuser rights only to people who should manage accounts.
- Locked out? Five failed sign-ins lock the address out for one hour; a staff user can lift it sooner from Users (Unlock). As root,
babybuddy-reset-passwordclears the lockout, reactivates the account if it was deactivated or expired, recreates it if it was deleted, sets a new generated password and writes it to the credentials file (it never prints it):
sudo babybuddy-reset-password
sudo grep '^ADMIN_PASSWORD=' /var/lib/cloudimg/babybuddy-credentials.txt
Pass a user name (sudo babybuddy-reset-password grandma) to reset another account; its new password is written to the same file.
- API: Baby Buddy's REST API (
/api/) accepts a session or a token. Each user's API key is under the user menu, then Settings; treat it like a password.
Step 10: Check the security controls
Baby Buddy, its first-boot unit and nginx carry IPAddressDeny for the metadata addresses, first boot recorded that it could not reach the metadata service from inside its own unit, Baby Buddy runs sandboxed as babybuddy, the data and keys are private, the certificate is an end-entity certificate for this host, and SSH runs with the cloudimg pins:
systemctl show -p IPAddressDeny babybuddy.service babybuddy-firstboot.service nginx.service
systemctl show -p User -p ProtectSystem -p ProtectHome -p NoNewPrivileges babybuddy.service
sudo grep -o 'IMDS-REFUSED-IN-FIRSTBOOT.*' /var/log/cloudimg-firstboot.log
sudo stat -c '%a %U:%G %n' /var/lib/babybuddy /var/lib/babybuddy/data/db.sqlite3 /var/lib/babybuddy/secret_key /etc/ssl/babybuddy/babybuddy.key
openssl x509 -in /etc/ssl/babybuddy/babybuddy.crt -noout -ext basicConstraints
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|x11forwarding) '
IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32
IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32
IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32
User=babybuddy
ProtectHome=yes
ProtectSystem=strict
NoNewPrivileges=yes
IMDS-REFUSED-IN-FIRSTBOOT (http 000; IPAddressDeny in effect)
700 babybuddy:babybuddy /var/lib/babybuddy
600 babybuddy:babybuddy /var/lib/babybuddy/data/db.sqlite3
600 babybuddy:babybuddy /var/lib/babybuddy/secret_key
600 root:root /etc/ssl/babybuddy/babybuddy.key
X509v3 Basic Constraints: critical
CA:FALSE
permitrootlogin no
passwordauthentication no
x11forwarding no
Maintenance
- Operating system updates: unattended security upgrades are on. Reboot when
/var/run/reboot-requiredexists; Baby Buddy and nginx start again by themselves. - Baby Buddy updates: Baby Buddy is installed from a pinned release and a hashed lock into
/opt/babybuddy. To move to a newer release, launch the newer version of this image when the listing offers it and move your data across (below). - Backups: everything Baby Buddy keeps is in
/var/lib/babybuddy: the database (data/db.sqlite3), the secret key and uploaded photos (media/). Stop Baby Buddy (sudo systemctl stop babybuddy) and copy the directory, or take an EBS snapshot of the root volume. - Logs:
sudo journalctl -u babybuddyfor Baby Buddy,/var/log/nginx/for web requests,/var/log/cloudimg-firstboot.logfor first boot. - What the instance contacts: Baby Buddy itself makes no outbound requests. Everything else comes from the operating system: the AWS Systems Manager agent, snapd, unattended upgrades and the time service. Disable the SSM agent if you do not use Session Manager.
Troubleshooting
- The web page does not load right after launch: first boot is still running.
sudo journalctl -u babybuddy-host.service -u babybuddy-firstboot.service -fshows its progress. - Browser shows "connection closed": nginx answers only the configured name and redirects the instance's own IP addresses to it. Use
BABYBUDDY_URL, or runsudo babybuddy-set-hostname <name>for a new name. - "CSRF verification failed" on sign-in: you reached Baby Buddy under a name it is not configured for (for example through your own proxy). Run
sudo babybuddy-set-hostnamewith the name your browser uses. - Locked out after failed sign-ins: the lock lifts after one hour; or run
sudo babybuddy-reset-password(Step 9). - The end-to-end check:
sudo babybuddy-selftestsigns in, reads the API and printsround-trip OKwhen everything works.
Licensing
Baby Buddy is licensed under the BSD 2-Clause License. The licence text is on the instance in /opt/babybuddy/app/LICENSE, and the source of this release is at github.com/babybuddy/babybuddy/releases/tag/v2.11.0. cloudimg is not affiliated with, endorsed by or sponsored by the Baby Buddy project or its contributors.
Support
cloudimg provides 24/7 technical support for this image by email and live chat: deployment, first login, HTTPS and DNS setup, upgrades and troubleshooting. Contact support@cloudimg.co.uk.