Mojolicious and Docker part 2
I my last post mojolicious-and-docker I have explained how to build and Docker image and to spin up which holds an Mojo app in this one I want to add show how to: Add an data base image to the docker compose file Create an network between the and the app Create and persists your db Start the Docker in the containers in the correct order So lets start. In the compose.yaml add the following lines: database: image: mariadb ports: - "3306:3306" This code snippet will the db image. Now add the following lines at the same level with ports in the compose file. environment: MARIADB_ROOT_PASSWORD: ${DB_PASSWORD} MARIADB_DATABASE: ${DB_NAME} MARIADB_PASSWORD: ${DB_PASSWORD} MARIADB_USER: ${DB_USER} This value are red from the .env file defined in at the same level with the compose file. The content of that files is similar to this: MARIADB_ROOT_PASSWORD= DB_HOST= DB_USER= DB_PASSWORD= DB_NAME= As side note on local machine I make this entry into '/etc/hosts' to be able to connect to the db from outside the Docker container. 127.0.0.1 database Because you do not want to lose date mount the db files for persistence using this line: volumes: - ./maria-data:/var/lib/mysql At this point you have an Maria db image that we know it will not lose data but nothing is actually in there. Lets address this by mounting an new volume: - ./my_app/migrations/000_create_db.sql:/docker-entrypoint-initdb.d/000_create_db.sql Open an editor and this lines in the 000_create_db.sql script: CREATE DATABASE IF NOT EXISTS my_app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; This will run and create the db when you run docker compose up. In order to make sure that everything cheeks out we need to: add health check, create and network and make sure that the app starts after the db: So to add the health check drop this lines healthcheck: test: ["CMD-SHELL", "mariadb -u${DB_USER} -p${DB_PASSWORD} -e 'SELECT 1'"] interval: 10s timeout: 5s retries: 5 start_period: 30s Then both services database and web and this line: networks: - my-app-net Then at the bottom of the file define the network: networks: my-app-net: To make sure that the app wont break add this in the web service: depends_on: database: condition: service_healthy This will ensure that the app will start only after the db. At this point we can start to think about integrating the db in into our app. First thing is first lets add all the Perl libraries into the cpan file: requires 'File::Slurper' => '0.014'; requires 'Mojolicious' => '9.39',; requires 'Dotenv' => '0.002'; requires 'DBI' => ' 1.647'; requires 'DBD::MariaDB' => '1.23'; requires 'Dotenv' => '0.002'; requires 'Try::Tiny' => '0.32'; requires 'Digest' => '1.20'; requires 'Digest::SHA' => '6.04'; requires 'Mojolicious::Plugin::Authentication' => '1.39'; requires 'Digest::Bcrypt' => '1.212'; requires 'DBD::Mock::Session::GenerateFixtures' => '0.11'; Now run carton install locally this will update the the cpanfile.snapshot and take of the dependencies. Now if we type docker compose build an new image will be create with all the requires libraries baked in. Now spin the composition with docker compose up You should see something similar with this: ✔ Container my_app-database-1 Created 0.0s ✔ Container my_app-web-1 Created No its time to put some meat and the bones at some Perl code in our app and some tables we want to use: So create this table in the db: CREATE TABLE IF NOT EXISTS users ( id MEDIUMINT NOT NULL AUTO_INCREMENT, plugin VARCHAR(30) NOT NULL DEFAULT 'Auth', username VARCHAR(30) UNIQUE NOT NULL, user_password VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, salt VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, is_admin TINYINT(1) NOT NULL DEFAULT '0', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE INDEX idx_plugin ON users(plugin); CREATE INDEX idx_username ON users(username); CREATE INDEX idx_user_password ON users(user_password); Now in the main controller add this lines MyApp.pm inside sub start up: # Router my $r = $self->routes; # # # Normal route to controller $r->get('/')->to('Example#welcome'); #load the login form $r->get('/login')->to( controller => 'Login', action => 'login'

I my last post mojolicious-and-docker I have explained how to build and Docker image and to spin up which holds an Mojo app in this one I want to add show how to:
- Add an data base image to the docker compose file
- Create an network between the and the app
- Create and persists your db
- Start the Docker in the containers in the correct order
So lets start.
In the compose.yaml
add the following lines:
database:
image: mariadb
ports:
- "3306:3306"
This code snippet will the db image.
Now add the following lines at the same level with ports in the compose file.
environment:
MARIADB_ROOT_PASSWORD: ${DB_PASSWORD}
MARIADB_DATABASE: ${DB_NAME}
MARIADB_PASSWORD: ${DB_PASSWORD}
MARIADB_USER: ${DB_USER}
This value are red from the .env
file defined in at the same level with the compose file. The content of that files is similar to this:
MARIADB_ROOT_PASSWORD=
DB_HOST=
DB_USER=
DB_PASSWORD=
DB_NAME=
As side note on local machine I make this entry into '/etc/hosts' to be able to connect to the db from outside the Docker container.
127.0.0.1 database
Because you do not want to lose date mount the db files for persistence using this line:
volumes:
- ./maria-data:/var/lib/mysql
At this point you have an Maria db image that we know it will not lose data but nothing is actually in there. Lets address this by mounting an new volume:
- ./my_app/migrations/000_create_db.sql:/docker-entrypoint-initdb.d/000_create_db.sql
Open an editor and this lines in the 000_create_db.sql
script:
CREATE DATABASE IF NOT EXISTS my_app
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
This will run and create the db when you run docker compose up
.
In order to make sure that everything cheeks out we need to: add health check, create and network and make sure that the app starts after the db:
So to add the health check drop this lines
healthcheck:
test: ["CMD-SHELL", "mariadb -u${DB_USER} -p${DB_PASSWORD} -e 'SELECT 1'"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
Then both services database and web and this line:
networks:
- my-app-net
Then at the bottom of the file define the network:
networks:
my-app-net:
To make sure that the app wont break add this in the web service:
depends_on:
database:
condition: service_healthy
This will ensure that the app will start only after the db.
At this point we can start to think about integrating the db in into our app. First thing is first lets add all the Perl libraries into the cpan file:
requires 'File::Slurper' => '0.014';
requires 'Mojolicious' => '9.39',;
requires 'Dotenv' => '0.002';
requires 'DBI' => ' 1.647';
requires 'DBD::MariaDB' => '1.23';
requires 'Dotenv' => '0.002';
requires 'Try::Tiny' => '0.32';
requires 'Digest' => '1.20';
requires 'Digest::SHA' => '6.04';
requires 'Mojolicious::Plugin::Authentication' => '1.39';
requires 'Digest::Bcrypt' => '1.212';
requires 'DBD::Mock::Session::GenerateFixtures' => '0.11';
Now run carton install locally this will update the the cpanfile.snapshot and take of the dependencies.
Now if we type docker compose build
an new image will be create with all the requires libraries baked in.
Now spin the composition with docker compose up
You should see something similar with this:
✔ Container my_app-database-1 Created 0.0s
✔ Container my_app-web-1 Created
No its time to put some meat and the bones at some Perl code in our app and some tables we want to use:
So create this table in the db:
CREATE TABLE IF NOT EXISTS users (
id MEDIUMINT NOT NULL AUTO_INCREMENT,
plugin VARCHAR(30) NOT NULL DEFAULT 'Auth',
username VARCHAR(30) UNIQUE NOT NULL,
user_password VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
salt VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
is_admin TINYINT(1) NOT NULL DEFAULT '0',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE INDEX idx_plugin ON users(plugin);
CREATE INDEX idx_username ON users(username);
CREATE INDEX idx_user_password ON users(user_password);
Now in the main controller add this lines MyApp.pm inside sub start up:
# Router
my $r = $self->routes;
# # # Normal route to controller
$r->get('/')->to('Example#welcome');
#load the login form
$r->get('/login')->to(
controller => 'Login',
action => 'login'
);
#submit the login form
$r->post('/login')->to(
controller => 'Login',
action => 'user_login'
);
my $auth_required = $r->under('/')->to('Login#user_exists');
$auth_required->get('/welcome')->to(
controller => 'User', action => 'welcome',
)
Now lets add an the login link to templates/example/welcome.html.ep
:
<%= $msg %>
<%= link_to 'here' => '/login' %> to login.
Now lets define the login controller by creating this file:
package My_App::Controller::Login;
use Mojo::Base 'Mojolicious::Controller', -signatures;
use Data::Dumper;
use Digest;
use MIME::Base64;
use Mojolicious::Plugin::Authentication;
use lib 'lib';
use DBI;
use DBD::MariaDB;
my $dsn = "DBI:MariaDB:database=$ENV{DB_NAME};host=$ENV{DB_HOST};port={$ENV{DB_PORT}";
my $dbh = DBI->connect($dsn, $ENV{DB_USER}, $ENV{DB_PASSWORD});
sub login($self) {
$self->render(
template => 'login',
error => $self->flash('error')
);
}
sub user_login($self) {
# From the form
my $password = $self->param('password');
my $username = $self->param('username');
# auth plugin setup
$self->app->plugin(
'authentication' => {
autoload_user => 1,
wickedapp => 'YouAreLogIn',
load_user => sub($c, $user_id) {
if ($user_id) {
my $query = "<";
SELECT t1.id,
t1.username,
t1.user_password,
t1.salt,
t1.is_admin
FROM users t1 WHERE t1.id = ?
SQL
my $sth = $dbh->prepare($query);
$sth->execute($user_id);
my $user = $sth->fetchrow_hashref();
return $user
}
return;
},
validate_user => sub($c, $user, $pass, $extradata) {
my $user_key = $self->validate_user_login($user, $password);
}
}
);
my $auth_key = $self->authenticate($username, $password);
if ($auth_key) {
$self->flash(message => 'Login Success.');
$self->session(user => $auth_key);
return $self->redirect_to('/welcome');
} else {
$self->flash(error => 'Invalid username or password.');
$self->redirect_to('login');
}
}
# validate the user login
sub validate_user_login {
my ($self, $username, $password, $extradata) = @_;
SELECT t1.id,
t1.username,
t1.user_password,
t1.salt,
t1.is_admin
FROM users t1 WHERE t1.username = ?
SQL
my $sth = $dbh->prepare($query);
$sth->execute($password);
my $user = $sth->fetchrow_hashref();
my $id = $user->{id};
my $db_password = $user->{user_password};
my $salt = $user->{salt};
if (!defined $id) {
return 0;
} else {
return validate_password($password, $db_password, $salt) ? $id : undef;
}
return 1;
}
sub validate_password($form_password, $db_password, $salt) {
# $salt = decode_base64($salt);
my $cost = 12;
my $bcrypt = Digest->new(
'Bcrypt',
cost => $cost,
salt => decode_base64($salt)
);
if ($bcrypt->add($form_password)->b64digest() eq $db_password) {
return 1;
}
return 0;
}
sub user_exists($c) {
if ($c->session('user')) {
return 1;
} else {
$c->redirect_to('login');
}
}
1;
Now lets add the login form:
class="container">
class="card col-sm-6 mx-auto">
class="card-header text-center">
User Sign In
/>
/>
% if ($error) {
class="error" style="color: red">
<%= $error %>
%}