NestJS applications with TypeORM default to localhost connections for development but break in production when the database is a remote managed instance. The failure is usually silent: the app starts, passes the health check because the root endpoint does not query the database, then crashes on the first request that hits a repository. By the time you see the error in logs, the deploy is marked as successful and users are getting 500 errors.
Managed databases solve the operational burden of backups, scaling, and failover, but they require explicit configuration: connection strings with TLS, environment variables injected at runtime, and migrations that run before the app starts. Getting this right on the first deploy saves hours of debugging connection timeouts and authentication failures.
Provision a managed MySQL database
PandaStack offers managed MySQL via KubeBlocks on Kubernetes. Provision an instance with the CLI:
panda databases create \
--name nestjs-mysql \
--engine mysql \
--version 8The platform creates a MySQL 8 instance with 1 CPU, 2 GB RAM, 10 GB disk, daily backups, and 7-day retention on the free tier. The command returns a connection string:
mysql://user:password@host.pandastack.app:3306/nestjs-mysql?ssl-mode=REQUIREDSSL is enforced, so clients must connect over TLS. The ssl-mode=REQUIRED parameter ensures the connection fails if TLS cannot be established, which prevents accidental plaintext connections.
Configure TypeORM to read DATABASE_URL from the environment
NestJS projects using TypeORM typically have a ormconfig.ts or inline configuration in app.module.ts. The development config hardcodes localhost:
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'password',
database: 'test',
entities: [User, Post],
synchronize: true,
});This works locally but fails in production because the database is not on localhost. The correct approach is to read the connection string from DATABASE_URL:
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot(),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: 'mysql',
url: configService.get<string>('DATABASE_URL'),
entities: [User, Post],
synchronize: false, // NEVER true in production
ssl: { rejectUnauthorized: false }, // Required for managed MySQL
}),
}),
],
})
export class AppModule {}The url parameter parses the connection string and extracts host, port, username, password, and database. The ssl option enables TLS. Setting rejectUnauthorized: false allows self-signed certificates, which some managed providers use. If your database uses a CA-signed certificate, you can remove this option.
synchronize: false is critical. In development, synchronize: true auto-creates tables from entity definitions, which is convenient. In production, it drops and recreates tables on every deploy, deleting all data. Always use migrations in production.
Link the database to your NestJS app
Instead of manually copying the connection string into environment variables, link the database when creating the project. The platform injects DATABASE_URL automatically:
curl -X POST https://api.pandastack.io/v1/projects \
-H "Authorization: Bearer $PANDASTACK_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"slug": "container",
"name": "nestjs-api",
"repositoryName": "yourname/nestjs-api",
"branch": "main",
"autoDeploy": true,
"linkDatabase": "nestjs-mysql",
"startCommand": "node dist/main"
}'The platform:
- 1Clones the repo and runs
npm install - 2Builds the app with
npm run build(compiles TypeScript todist/) - 3Injects
DATABASE_URLfrom the linked database - 4Starts the server with
node dist/main
The app reads DATABASE_URL from process.env, connects to MySQL over TLS, and runs queries. No hardcoded credentials, no manual copy-paste.
Fix port binding so the health check passes
NestJS defaults to localhost:3000, which makes the app unreachable from outside the container. Kubernetes health checks hit the container's IP address, not localhost, so the probe times out and the deploy fails.
Change main.ts to bind to all interfaces:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000, '0.0.0.0');
}
bootstrap();The '0.0.0.0' parameter tells the server to accept connections on any network interface. Push the change and redeploy. The health check passes, and the deploy completes.
Run database migrations before the app starts
TypeORM has a built-in migration system. Generate a migration:
npm run typeorm migration:generate -- -n CreateUsersThis creates a migration file in src/migrations/. Run it locally to test:
npm run typeorm migration:runIn production, migrations need to run after the database is provisioned but before the app starts handling requests. The cleanest approach is to chain the migration command with the start command:
{
"startCommand": "npm run typeorm migration:run && node dist/main"
}If a migration fails, the app never starts, and the deploy is marked as failed. The previous version keeps running, which prevents a broken schema from taking down the API.
Alternatively, create a migrate.sh script:
#!/bin/sh
npm run typeorm migration:run
exec node dist/mainMake it executable:
chmod +x migrate.shThen use it as the start command:
{
"startCommand": "./migrate.sh"
}Handle connection pooling to avoid "too many connections" errors
MySQL has a connection limit. Free-tier managed databases allow 50 concurrent connections; paid tiers allow 300 or 1000 depending on the plan. If your NestJS app creates a new connection for every request and never closes them, you will hit the limit and start seeing ER_CON_COUNT_ERROR: Too many connections.
TypeORM uses a connection pool by default, but you need to configure the pool size to match your deployment. For a free-tier container with 0.25 CPU and 512 MB RAM, a pool of 5 connections is reasonable:
TypeOrmModule.forRootAsync({
useFactory: (configService: ConfigService) => ({
type: 'mysql',
url: configService.get<string>('DATABASE_URL'),
entities: [User, Post],
synchronize: false,
ssl: { rejectUnauthorized: false },
extra: {
connectionLimit: 5,
},
}),
});The connectionLimit parameter sets the maximum number of connections the pool maintains. Requests queue if all connections are busy. This prevents the app from exhausting the database's connection limit.
For paid-tier containers with more CPU, increase the pool size to match the available concurrency. A container with 2 CPU can handle 20–50 concurrent requests, so a pool of 10–20 connections is appropriate.
Deploy via the deploy button for template repos
If your NestJS app is a starter template, add a deploy button to the README:
[](https://dashboard.pandastack.io/deploy?repo=yourname/nestjs-starter&type=container&lang=nodejs)Users who click the badge land on the deploy screen. If the repo has pandastack.json with an env array that declares DATABASE_URL, the screen prompts for it. Otherwise, they need to provision a database and link it manually.
The cleanest onboarding path is to include pandastack.json:
{
"type": "container",
"name": "nestjs-api",
"language": "nodejs",
"buildCommand": "npm run build",
"startCommand": "npm run typeorm migration:run && node dist/main",
"env": [
{
"key": "DATABASE_URL",
"description": "MySQL connection string (mysql://user:pass@host:3306/db?ssl-mode=REQUIRED)"
}
]
}The deploy screen prompts for DATABASE_URL, the user fills it in, and the app boots correctly.
Add environment variables for secrets and API keys
Your NestJS app probably needs more than DATABASE_URL — JWT signing secrets, third-party API keys, feature flags. Add them via the API or CLI:
panda projects env <project-id> --set JWT_SECRET=random-256-bit-string
panda projects env <project-id> --set STRIPE_API_KEY=sk_live_xxxThe variables are encrypted at rest and injected at runtime. Access them in your code via ConfigService:
const jwtSecret = this.configService.get<string>('JWT_SECRET');Secrets are masked in the dashboard and CLI output, so you cannot accidentally leak them in screenshots or logs.
Monitor query performance and slow queries
TypeORM logs queries when logging: true is set in the config. In production, this generates massive log volume and slows down the app. Instead, enable logging only for slow queries:
TypeOrmModule.forRootAsync({
useFactory: (configService: ConfigService) => ({
type: 'mysql',
url: configService.get<string>('DATABASE_URL'),
logging: ['error', 'warn'],
maxQueryExecutionTime: 1000, // Log queries slower than 1 second
}),
});Slow queries appear in the application logs. PandaStack captures logs and makes them searchable in the dashboard under the Logs tab. You can filter for Slow query to find performance bottlenecks.
References
- [NestJS TypeORM integration](https://docs.nestjs.com/techniques/database)
- [TypeORM migrations guide](https://typeorm.io/migrations)
- [PandaStack managed databases](https://docs.pandastack.io/databases/)