This guide explains how to run database migrations in Docker environment.
After building the Docker image, you need to run three types of migrations:
- migrate:generate - Generate new Drizzle migration files (development only)
- migrate - Run pending Drizzle migrations to create/update MySQL schema
- migrate:data - One-time migration of data from LanceDB to MySQL
- Docker and Docker Compose installed
.envfile configured with required variables- LanceDB data directory available (if migrating existing data)
Create a .env file in the project root:
# MySQL Configuration
MYSQL_ROOT_PASSWORD=aimo_root_password
MYSQL_DATABASE=aimo
MYSQL_USER=aimo
MYSQL_PASSWORD=aimo_password
MYSQL_HOST=mysql
MYSQL_PORT=3306
# LanceDB Configuration
LANCEDB_STORAGE_TYPE=local
LANCEDB_PATH=./lancedb_data
# Required for server
JWT_SECRET=your-super-secret-key-at-least-32-characters-long
OPENAI_API_KEY=sk-xxx# Start MySQL service
docker compose -f docker-compose.migrate.yml up -d mysql
# Wait for MySQL to be ready (check health)
docker compose -f docker-compose.migrate.yml ps# Run pending migrations to create MySQL schema
docker compose -f docker-compose.migrate.yml run --rm migrate \
node apps/server/dist/scripts/docker-migrate.js migrate# Migrate existing data from LanceDB to MySQL
docker compose -f docker-compose.migrate.yml run --rm migrate \
node apps/server/dist/scripts/docker-migrate.js migrate-data# Stop and remove migration containers
docker compose -f docker-compose.migrate.yml downdocker build -f Dockerfile.migrate -t aimo-migrate:latest .# Create network
docker network create aimo-network
# Start MySQL
docker run -d \
--name aimo-mysql \
--network aimo-network \
-e MYSQL_ROOT_PASSWORD=aimo_root_password \
-e MYSQL_DATABASE=aimo \
-e MYSQL_USER=aimo \
-e MYSQL_PASSWORD=aimo_password \
-p 3306:3306 \
-v aimo_mysql_data:/var/lib/mysql \
mysql:8.0
# Wait for MySQL to be ready
docker exec aimo-mysql mysqladmin ping -h localhost --silent# Run Drizzle migrations
docker run --rm \
--network aimo-network \
-e MYSQL_HOST=aimo-mysql \
-e MYSQL_PORT=3306 \
-e MYSQL_USER=aimo \
-e MYSQL_PASSWORD=aimo_password \
-e MYSQL_DATABASE=aimo \
-e JWT_SECRET=your-super-secret-key-at-least-32-characters-long \
-e OPENAI_API_KEY=sk-xxx \
-v $(pwd)/lancedb_data:/app/lancedb_data \
aimo-migrate:latest \
node apps/server/dist/scripts/docker-migrate.js migrate# Migrate data from LanceDB to MySQL
docker run --rm \
--network aimo-network \
-e MYSQL_HOST=aimo-mysql \
-e MYSQL_PORT=3306 \
-e MYSQL_USER=aimo \
-e MYSQL_PASSWORD=aimo_password \
-e MYSQL_DATABASE=aimo \
-e JWT_SECRET=your-super-secret-key-at-least-32-characters-long \
-e OPENAI_API_KEY=sk-xxx \
-e LANCEDB_PATH=/app/lancedb_data \
-v $(pwd)/lancedb_data:/app/lancedb_data \
aimo-migrate:latest \
node apps/server/dist/scripts/docker-migrate.js migrate-data# Run all migrations
make migrate
# Run only schema migrations
make migrate-schema
# Run only data migration
make migrate-dataGenerates new Drizzle migration files based on schema changes.
# Not typically needed in production
docker compose -f docker-compose.migrate.yml run --rm migrate \
node apps/server/dist/scripts/docker-migrate.js generateWhen to use:
- After modifying Drizzle schema files in
apps/server/src/db/schema/ - During development to create new migration files
Output:
- New migration files in
apps/server/drizzle/directory
Runs pending Drizzle migrations to create/update MySQL schema.
docker compose -f docker-compose.migrate.yml run --rm migrate \
node apps/server/dist/scripts/docker-migrate.js migrateWhen to use:
- First deployment to create all MySQL tables
- After pulling new code with schema changes
- Before starting the application server
What it does:
- Creates tables: users, memos, categories, tags, memo_relations, attachments, etc.
- Creates indexes and foreign key constraints
- Tracks migration history in
__drizzle_migrationstable
One-time migration of existing data from LanceDB to MySQL.
docker compose -f docker-compose.migrate.yml run --rm migrate \
node apps/server/dist/scripts/docker-migrate.js migrate-dataWhen to use:
- ONLY ONCE when migrating from LanceDB-only to hybrid MySQL+LanceDB architecture
- After running schema migrations (step 2)
What it does:
- Reads all scalar data from LanceDB tables
- Inserts scalar data into MySQL tables
- Preserves LanceDB tables unchanged (they still contain complete records)
- Idempotent: skips records that already exist in MySQL
- Backup your LanceDB data before running!
- Run with
--dry-runflag first to preview changes - Can be run multiple times safely (skips existing records)
# 1. Clone repository
git clone <repo-url>
cd aimo
# 2. Configure environment
cp .env.example .env
# Edit .env with your values
# 3. Start MySQL
docker compose -f docker-compose.migrate.yml up -d mysql
# 4. Wait for MySQL to be ready
sleep 10
# 5. Run schema migrations
docker compose -f docker-compose.migrate.yml run --rm migrate \
node apps/server/dist/scripts/docker-migrate.js migrate
# 6. Migrate existing data (if any)
docker compose -f docker-compose.migrate.yml run --rm migrate \
node apps/server/dist/scripts/docker-migrate.js migrate-data
# 7. Start application
docker compose up -d
# 8. Verify
curl http://localhost:3000/health# Check MySQL is running
docker compose -f docker-compose.migrate.yml ps
# Check MySQL logs
docker compose -f docker-compose.migrate.yml logs mysql
# Test connection
docker exec aimo-mysql mysql -uaimo -paimo_password -e "SELECT 1"# Check migration logs
docker compose -f docker-compose.migrate.yml logs migrate
# Manually connect to MySQL to check state
docker exec -it aimo-mysql mysql -uaimo -paimo_password aimo
# Check migration history
SELECT * FROM __drizzle_migrations;
# Reset migrations (DANGEROUS - only for development)
DROP DATABASE aimo;
CREATE DATABASE aimo;# Run in dry-run mode first
docker compose -f docker-compose.migrate.yml run --rm migrate \
tsx apps/server/src/scripts/migrate-lancedb-to-mysql.ts --dry-run
# Check specific table
docker exec -it aimo-mysql mysql -uaimo -paimo_password -e "SELECT COUNT(*) FROM aimo.memos"
# View migration stats
docker compose -f docker-compose.migrate.yml run --rm migrate \
tsx apps/server/src/scripts/migrate-lancedb-to-mysql.ts --table=memos- Backup LanceDB data directory
- Backup MySQL database (if exists)
- Configure
.envwith production values - Run schema migrations first
- Test application startup
- Run data migration (if needed)
- Verify data integrity
- Monitor application logs
- Update DNS/load balancer to new deployment
If migration fails:
# 1. Stop application
docker compose down
# 2. Restore MySQL backup (if needed)
docker exec -i aimo-mysql mysql -uaimo -paimo_password aimo < backup.sql
# 3. Restore LanceDB data (if modified)
rm -rf lancedb_data
cp -r lancedb_data.backup lancedb_data
# 4. Restart with previous version
git checkout <previous-version>
docker compose up -d- LanceDB tables remain unchanged: They still contain complete records (scalar + vector)
- MySQL is the primary data source: Used for all CRUD operations
- Dual-write strategy: Both databases are kept in sync
- No downtime required: Can run migrations before switching traffic
- Idempotent migrations: Safe to run multiple times
For issues or questions:
- Check logs:
docker compose logs - Review migration docs:
apps/server/MIGRATION.md - Open issue: https://github.com/ximing/aimo/issues