Knex
We’re going to use the Knex library to connect to our database from the server. We’ll also use it to handle database migrations and seeding.
Install Knex
We can install Knex using npm inside the server container:
# Terminal
docker exec -it project-server bashThen we can use npm to install the knex library, along with the pg library for connecting to Postgres databases. This should be done in the /app/server directory inside the container:
# Docker project-server Terminal
npm install knex pgTip
You can replace pg with the database engine of your choice. Modifications must be made below to match that engine; refer to the Knex documentation.
Once Knex is installed, we’ll run a few additional commands inside of the container to configure it.
Create Knexfile
The Knex library uses a special file called a knexfile to store some configuration information. We can create it using this command:
# Docker project-server Terminal
npx knex initTip
In the documentation for Knex it will reference these commands as just knex. However, since they are installed by an npm package, they are not directly accessible in the system path inside the container. So, to get around that, we can prefix the command with npx to run commands from within an npm package. npx documentation
This should create a file server/knexfile.js.
Once that is done, we can close the terminal connected to the container using the exit command.
We’ll replace the default contents of the server/knexfile.js with the following:
// server/knexfile.js
/**
* @type { Object.<string, import("knex").Knex.Config> }
*/
module.exports = {
development: {
client: 'pg',
connection: {
host: process.env.POSTGRES_HOST,
port: process.env.POSTGRES_PORT,
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
database: process.env.POSTGRES_DB,
},
migrations: {
tableName: 'migrations',
},
},
production: {
client: 'pg',
connection: {
host: process.env.POSTGRES_HOST,
port: process.env.POSTGRES_PORT,
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
database: process.env.POSTGRES_DB,
},
migrations: {
tableName: 'migrations',
},
},
}This will use environment variables to configure the database in both development and production modes.
Pass Environment to Containers
Next we’ll need to reconfigure our containers to pass the environment from the .env file. To do this, we’ll edit the server service in the Docker Compose file:
# server
project-server:
build:
# location of Dockerfile
context: ./server
container_name: project-server
# set the user ID to match our user
user: "1000"
depends_on:
# requires database to start
- project-db
networks:
- project-network
# allow external connections (remove this in production)
- default
volumes:
# mount code into container
- ./server:/app/server
# maintain existing node_modules in container
- /app/server/node_modules
env_file:
# load environment file into container environment
- .envThis will load the entire contents of the .env file into the container’s environment. This is a bit different than how we handled the database container, since we only passed it a few variables. Refer to Docker Compose Environment Documentation for details.
Recreate Container
Once we’ve made that change, we need to recreate our container with the new environment.
# Terminal
docker compose up -dThis will give us access to the information needed to connect to the database.
Create Migration
We can use Knex to create a migration for our database. First, we must connect to it:
# Terminal
docker exec -it project-server bashThen we can create a migration:
# Docker project-server Terminal
npx knex migrate:make usersTip
Use the same user ID and group ID found earlier when setting up the projects.
This will create a file in the migrations directory that includes a timestamp and the name of the migration. The timestamp is included to ensure the migrations are applied in the correct order.
Add Users Table
Now, open the file created in the migrations directory and add the following content:
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
return knex.schema
.createTable('users', function (table) {
table.increments('id')
table.string('username', 20).unique().notNullable()
table.string('passhash', 60).notNullable()
table.string('name', 255).notNullable()
table.string('email', 255).notNullable()
table.timestamps()
})
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
return knex.schema
.dropTable('users')
};This will create a users table with a few columns. You can find more information about creating migrations with Knex in the Schema Builder Documentation.
Once we’ve created that file, we can apply the migration using the knex command line tool:
# Docker project-server Terminal
npx knex migrate:upIf everything works correctly, you’ll get a success message. You can check that the table exists in the database using various tools to query a Postgres database.
Create Seed
You can also use Knex to seed some initial data into the database. First, create a seed file from inside the project-server Docker container:
# Docker project-server Terminal
npx knex seed:make users_seedThis will create a file users_seed.js in the seeds directory. Add the following content to that file:
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.seed = async function(knex) {
// Get current date
const now = new Date().toISOString().slice(0, 19).replace('T', ' ')
// Deletes ALL existing entries
await knex('users').del()
await knex('users').insert([
{
id: 1,
username: 'admin',
passhash: '$2y$10$41NXCocgYzuOGgOB4aL1M.wg7mZT.P6NY9D9EbfdKu8xbHU8x0b3O',
name: "Administrator",
email: "admin@local.local",
created_at: now,
updated_at: now,
},
{
id: 2,
username: 'user',
passhash: '$2y$10$41NXCocgYzuOGgOB4aL1M.wg7mZT.P6NY9D9EbfdKu8xbHU8x0b3O',
name: "User",
email: "user@local.local",
created_at: now,
updated_at: now,
}
]);
};We can now apply that seed file using the knex command line tool:
# Docker project-server Terminal
npx knex seed:runYou should now be able to see a couple of entries in the users table of the database. You can check that these exist in the database using various tools to query a Postgres database.
Helpful Scripts
It is very helpful to have a script that will allow you to reset your database while working on it. I recommend adding the following reset to your package.json file for the server:
"scripts": {
"dev": "nodemon ./bin/www",
"start": "NODE_ENV=production node ./bin/www",
"reset": "knex migrate:rollback --all && knex migrate:latest && knex seed:run"
}With that script in the package.json file, you can use the following command to fully reset the database back to the default state:
# Docker project-server Terminal
npm run resetThis will undo all database migrations, redo them, and seed the database with your initial seed data.
Note
You may also wish to look into Objection.js, which is a great ORM that works well with Knex.