MySQL

Database management in a containerized homestack requires careful consideration of resource usage, schema evolution, and operational simplicity. Rather than using a managed MySQL service, I run MySQL as another container with a focus on minimal memory usage and schema-as-code using Skeema.

Why Self-Hosted MySQL?

For a HomeStack, self-hosted MySQL offers several advantages over managed services:

  • Cost control: No monthly fees or per-query charges
  • Resource efficiency: Tuned for low-memory environments
  • Full control: Custom configurations and direct access
  • Data locality: No external dependencies or network latency
  • Learning opportunity: Better understanding of MySQL internals

The trade-off is operational responsibility, but for a home lab this becomes a feature rather than a bug.

Memory-Optimized Configuration

My HomeStack runs on modest hardware, so MySQL is configured for minimal memory usage:

# low-memory-my.cnf
[mysqld]
# Core memory optimizations
innodb_buffer_pool_size=5M
innodb_log_buffer_size=256K
max_connections=10
key_buffer_size=8
thread_cache_size=0
host_cache_size=0

# Per-thread settings
thread_stack=256K
sort_buffer_size=32K
read_buffer_size=8200
read_rnd_buffer_size=8200
max_heap_table_size=16K
tmp_table_size=1K

# Disable performance schema
performance_schema = off

This configuration reduces MySQL's memory footprint from ~400MB to under 50MB while maintaining functionality for moderate workloads. The key optimizations:

  • Tiny buffer pool: 5MB instead of the default 128MB
  • Limited connections: 10 concurrent connections vs. default 151
  • Minimal caches: Disabled or drastically reduced cache sizes
  • No performance schema: Saves significant memory overhead

Container Setup

MySQL runs as a standard Docker container with persistent storage:

mysqldb:
  container_name: mysqldb
  environment:
    - MYSQL_ROOT_PASSWORD=${MYSQL_PWD}
  image: mysql:8.4
  networks:
    - mysqldb
  volumes:
    - ./mysqldb/low-memory-my.cnf:/etc/mysql/conf.d/low.cnf
    - do-vol-mysqldb:/var/lib/mysql
  restart: unless-stopped

The setup includes:

  • Custom configuration: Memory-optimized settings override defaults
  • Persistent volumes: Data survives container recreation
  • Isolated network: Separate network for database access
  • Environment-based secrets: Root password from encrypted environment

Schema Management with Skeema

The game-changer for my database workflow is Skeema, which treats database schema as declarative code rather than imperative migrations.

Schema-as-Code Structure

Each service's schema is defined as SQL CREATE statements:

migration-cli/
  databases/
    chores/
      user.sql
      chore_submission.sql
      goal.sql
    chatgpt/
      chat.sql
      message.sql
    tools/
      pipeline.sql

Here's an example table definition:

-- chores/user.sql
CREATE TABLE `user` (
  `id` binary(16) NOT NULL,
  `email_hash` binary(32) NOT NULL,
  `auth0_id` varchar(255) DEFAULT NULL,
  `role` enum('parent','child') NOT NULL,
  `display_name` varchar(100) NOT NULL,
  `profile_image_key` varchar(255) NULL,
  `profile_image_crop_data` json NULL,
  `family_id` binary(16) NOT NULL,
  `data` json NOT NULL,
  `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
  `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
  PRIMARY KEY (`id`),
  UNIQUE KEY `email_hash` (`email_hash`),
  KEY `idx_family_role` (`family_id`,`role`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Migration Workflow

Schema changes follow a simple declarative workflow:

# 1. Edit the .sql files to match desired state
vim databases/chores/user.sql

# 2. Preview changes
nhdc db migrate # this loads a container with bash to run skeema commands

# 3. Apply changes
skeema diff
skeema push

This approach has several advantages:

  • No migration files: Just modify the target schema
  • Automatic diffing: Skeema calculates required changes
  • Safe operations: Preview exactly what will change
  • Version controlled: Schema definitions live in git

Migration CLI Container

Schema management runs in a dedicated container:

FROM golang:latest as builder
RUN go install github.com/skeema/skeema@v1.12.0
COPY . /app
WORKDIR /app/databases
ENTRYPOINT ["/bin/bash"]

This provides a consistent environment with Skeema pre-installed, accessed via:

nhdc db migrate
# Equivalent to:
docker run -it -v `pwd`/migration-cli:/app --rm --env-file .env --network=mono_mysqldb migration-cli

Database Design Patterns

UUID Primary Keys

I use binary(16) UUIDs for primary keys across all tables:

`id` binary(16) NOT NULL,
-- Selected as: bin_to_uuid(id) AS id

Benefits:

  • Distributed-friendly: No coordination required for ID generation
  • Non-sequential: Harder to enumerate records
  • Portable: IDs work across database instances

JSON Columns for Flexibility

MySQL's JSON column type handles semi-structured data:

`data` json NOT NULL,
`profile_image_crop_data` json NULL

This provides schema flexibility without sacrificing query performance through generated columns and JSON path indexing when needed.

Soft Deletes

Most tables include soft delete support:

`deleted_at` datetime(6) DEFAULT NULL,

This preserves referential integrity and enables data recovery without complex backup restoration.