Search code examples
node.jspostgresqldockersveltekitvps

Can't build Docker image with Postgresql


I'm trying to build the Docker image for deploying to a VPS (pulling it from DockerHub), but it gives me an error all the time, I thin related to the connection of the Database instance. I tried to add comments to show my thought process. I'm new to Docker so just trying to figure it out:

Here's the Dockerfile:

FROM node:22 AS builder

# Install pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate

WORKDIR /app

# Copy package files first to leverage cache
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

# Copy rest of the files
COPY . .
RUN pnpm build

FROM node:22 AS production
WORKDIR /app

RUN corepack enable && corepack prepare pnpm@latest --activate

# Copy necessary files from builder
COPY --from=builder /app/package.json /app/pnpm-lock.yaml ./
COPY --from=builder /app/build ./build

# Install production dependencies only
RUN pnpm install --prod --frozen-lockfile

# Add a non-root user for security
#RUN addgroup --system --gid 1001 nodejs \
#   && adduser --system --uid 1001 nodejs
#USER nodejs

EXPOSE 3000
CMD ["node", "build/index.js"]

The docker-compose.yaml

services:
  app:
    build: .
    image: pulso_test
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://root:mysecretpassword@db:5432/pulso_dev
      - NODE_ENV=production
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: postgres
    environment:
      - POSTGRES_USER=root
      - POSTGRES_PASSWORD=mysecretpassword
      - POSTGRES_DB=pulso_dev
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432" 
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U root -d pulso_dev"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 60s
    restart: unless-stopped

volumes:
  postgres_data:

And when I try to run the command:

docker build -t pulsodenieve-vps:latest .

This log appears:

[+] Building 21.6s (12/17)                                                                                                                   docker:desktop-linux
 => [internal] load build definition from Dockerfile                                                                                                         0.0s
 => => transferring dockerfile: 859B                                                                                                                         0.0s 
 => [internal] load metadata for docker.io/library/node:22                                                                                                   6.7s 
 => [auth] library/node:pull token for registry-1.docker.io                                                                                                  0.0s 
 => [internal] load .dockerignore                                                                                                                            0.0s
 => => transferring context: 143B                                                                                                                            0.0s 
 => [builder 1/7] FROM docker.io/library/node:22@sha256:ae2f3d4cc65d251352eca01ba668824f651a2ee4d2a37e2efb22649521a483fd                                     0.0s 
 => => resolve docker.io/library/node:22@sha256:ae2f3d4cc65d251352eca01ba668824f651a2ee4d2a37e2efb22649521a483fd                                             0.0s 
 => [internal] load build context                                                                                                                            0.1s 
 => => transferring context: 29.08kB                                                                                                                         0.0s 
 => CACHED [builder 2/7] RUN corepack enable && corepack prepare pnpm@latest --activate                                                                      0.0s
 => CACHED [builder 3/7] WORKDIR /app                                                                                                                        0.0s 
 => CACHED [builder 4/7] COPY package.json pnpm-lock.yaml ./                                                                                                 0.0s 
 => CACHED [builder 5/7] RUN pnpm install --frozen-lockfile                                                                                                  0.0s 
 => CACHED [builder 6/7] COPY . .                                                                                                                            0.0s 
 => ERROR [builder 7/7] RUN pnpm build                                                                                                                      14.6s 
------
 > [builder 7/7] RUN pnpm build:
0.329 ! The local project doesn't define a 'packageManager' field. Corepack will now add one referencing [email protected]+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0.
0.329 ! For more details about this field, consult the documentation at https://nodejs.org/api/packages.html#packagemanager
0.329
0.606
0.606 > [email protected] build /app
0.606 > vite build
0.606
2.248 NODE_ENV=production is not supported in the .env file. Only NODE_ENV=development is supported to create a development build of your project. If you need to set process.env.NODE_ENV, you can set it in the Vite config instead.
2.271 vite v6.0.3 building SSR bundle for production...
2.282 Using existing cloned repo
4.403 ℹ [paraglide] Compiling Messages into ./src/lib/paraglide
4.438 transforming...
12.97 "PostgresJsDatabase" is imported from external module "drizzle-orm/postgres-js" but never used in "src/lib/server/db/index.ts".
12.97 ✓ 2483 modules transformed.
13.15 rendering chunks...
14.41
14.41 node:internal/event_target:1101
14.41   process.nextTick(() => { throw err; });
14.41                            ^
14.41 Error: connect ECONNREFUSED 127.0.0.1:5432
14.41     at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1634:16) {
14.41   errno: -111,
14.41   code: 'ECONNREFUSED',
14.41   syscall: 'connect',
14.41   address: '127.0.0.1',
14.41   port: 5432
14.41 }
14.41
14.41 Node.js v22.13.1
14.56  ELIFECYCLE  Command failed with exit code 1.
------
Dockerfile:14
--------------------
  12 |     # Copy rest of the files
  13 |     COPY . .
  14 | >>> RUN pnpm build
  15 |
  16 |     FROM node:22 AS production
--------------------
ERROR: failed to solve: process "/bin/sh -c pnpm build" did not complete successfully: exit code: 1

View build details: docker-desktop://dashboard/build/desktop-linux/desktop-linux/k5srk2ca5wbzgursp9v4jsmzk

Here's my server/db/dbConfig.ts:

import { env } from '$env/dynamic/private';

export type DbConfig = {
  url: string;
  maxConnections?: number;
  ssl?: boolean;
};

export function getDbConfig(): DbConfig {
  const environment = env.NODE_ENV || 'development';
  
  const configs: Record<string, DbConfig> = {
    development: {
      url: env.DATABASE_URL || 'postgresql://root:mysecretpassword@localhost:5432/pulso_dev',
      maxConnections: 10
    },
    test: {
      url: env.TEST_DATABASE_URL || 'postgresql://root:mysecretpassword@localhost:5432/pulso_test',
      maxConnections: 5
    },
    production: {
      url: env.DATABASE_URL,
      ssl: true,
      maxConnections: 20
    }
  };

  const config = configs[environment];
  if (!config) throw new Error(`No database configuration for environment: ${environment}`);
  if (!config.url) throw new Error(`DATABASE_URL is not set for environment: ${environment}`);
  
  return config;
}

And server/db/index.ts:

import { drizzle, PostgresJsDatabase } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import { getDbConfig } from './config';
import * as users from './schemas/users';
import * as normalized from './schemas/normalized';
import * as availability from './schemas/availability';
import * as lessons from './schemas/lessons';
import * as bookings from './schemas/bookings';

const schema = { 
  ...users, 
  ...normalized, 
  ...availability, 
  ...lessons, 
  ...bookings 
} as const;

type DB = PostgresJsDatabase<typeof schema>;

let _db: DB | null = null;

export function createDb(): DB {
  const config = getDbConfig();
  const client = postgres(config.url, {
    max: config.maxConnections,
    ssl: config.ssl
  });

  return drizzle(client, { schema });
}

export function getDb(): DB {
  if (!_db) {
    _db = createDb();
  }
  return _db;
}

export function closeDb() {
  _db = null;
}

Solution

  • Well thank you to everybody who answered! In the end I came up with a dirty but efficient solution. The whole problem is that during build time the environment variables are not accessible (unless you use the ARG and ENV parameters in your Dockerfile file, but I don't like to bloat it), therefore all the database initialization, for example Stripe initialization or whatever other services that you may have that require those environmental variables, are gonna fail in build time. The idea is, try to adapt the code so instead of throwing an error, it accepts and manages situations where those env variables are null or not defined. For example in the database(postgres) initialization file (db/index.ts):

    import { drizzle } from 'drizzle-orm/postgres-js';
    import postgres from 'postgres';
    
    import * as users from '$lib/server/db/schemas/users'
    import * as normalized from '$lib/server/db/schemas/normalized'
    import * as availability from '$lib/server/db/schemas/availability';
    import * as lessons from '$lib/server/db/schemas/lessons';
    import * as bookings from '$lib/server/db/schemas/bookings';
    import { env } from 'process';
    
    
    function createDb() {
        const databaseUrl = env.DATABASE_URL;
        console.log('DATABASEURL: ', databaseUrl)
        if (!databaseUrl) throw new Error('DATABASE_URL is not set');
        
        // Skip DB connection during build
        if (process.env.NODE_ENV === 'build') {
            console.log('Build mode - skipping database connection');
            return null;
        }
    
        if (process.env.NODE_ENV === 'production') {
            console.log('Production mode - skipping database connection');
        }
    
        const client = postgres(databaseUrl);
        return drizzle(client, {
            schema: { ...users, ...normalized, ...availability, ...lessons, ...bookings }
        });
    }
    
    export const db = createDb();
    

    Adn then wherever this db needs to be used, just handle the scenario where it might not be initialized properly:

    import { eq } from "drizzle-orm";
    import { db } from ".";
    import { ageGroups, countries, currencies, languages, pricingModes, resorts, skillLevels, sports, timeUnits } from "./schemas/normalized";
    
    
    export async function getAllSkiResorts() {
        if (!db){
            console.error('Database Not Initialized')
            return [];
        } 
        return await db.select().from(resorts);
    }
    
    export async function getSkiResortsByCountry(countryId: number) {
        if (!db){
            console.error('Database Not Initialized')
            return [];
        } 
        return await db.select().from(resorts).where(eq(countries.id, countryId))
    }
    
    export async function getAllCountries() {
        if (!db){
            console.error('Database Not Initialized')
            return [];
        } 
        return await db.select().from(countries);
    }
    

    Also in the Dockerfile, you could set the "build" environment var just to perform cleaner logic:

    # Build stage
    FROM node:22-alpine AS builder
    RUN corepack enable && corepack prepare pnpm@latest --activate
    WORKDIR /app
    COPY package.json pnpm-lock.yaml ./
    RUN pnpm install
    COPY . .
    # Set build environment
    ENV NODE_ENV=build
    RUN pnpm build
    
    # Production stage
    FROM node:22-alpine
    RUN corepack enable && corepack prepare pnpm@latest --activate
    WORKDIR /app
    COPY package.json pnpm-lock.yaml ./
    RUN pnpm install --production
    COPY --from=builder /app/build ./build
    # Set production environment
    ENV NODE_ENV=production
    CMD ["node", "build/index.js"]
    EXPOSE 3000
    

    you just need to test and try to see where in your app, there is code that might create conflict during build and then adapt the logic to handle non existing values during buildtime. I hope this does the trick for someone in the same situation that I was!!