Saturday, January 25, 2025
Kamal & Fastify & Nodemailer
Let’s continue my Kamal exploration. Now I have tried to migrate fastify app to Kamal. I have simple portfolio app with contact form so it is not static site anymore and I have to pass some env variables because of contact form.
Dockerfile is quite simple:
FROM node:22-alpine AS base
RUN corepack enable yarn
WORKDIR /app
COPY package.json yarn.lock .yarnrc.yml ./
COPY .yarn ./.yarn
RUN yarn workspaces focus --production
FROM base AS production
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]
I am not going to show here how fastify works but here are important bits for Kamal:
// We need /up to return 200 OK
fastify.get('/up', (request, reply) => {
reply.type('text/html').send(`<p>OK</p>`);
});
// Important bit here to set host to 0.0.0.0 because we will use this in Docker
await fastify.listen({ port: 3000, host: '0.0.0.0' });
Here is excerpt from deploy.yml
that is important as well. I use port 3000
and I want site to be accessible via host and www subdomain:
proxy:
ssl: true
hosts:
- ffff.lt
- www.ffff.lt
app_port: 3000
Now if we want ffff.lt
to be redirected to www.ffff.lt
we can do it like
this (that’s not necessary proper way, but one way how to do it):
const redirectNonWwwToWww = (req, reply, done) => {
if (req.hostname === 'ffff.lt') {
const wwwUrl = `http://www.${req.hostname}${req.url}`;
reply.redirect(wwwUrl);
} else {
done();
}
};
fastify.addHook('preHandler', redirectNonWwwToWww);
Nodemailer & Hetzner
Now I use gmail with nodemailer to send mails from contact form to specific e-mail address. This is how it should work (some details here https://www.nodemailer.com/usage/using-gmail/) and it works for me:
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: 'your-email@gmail.com', // Your Gmail address
pass: 'your-app-password' // App password or email password
}
});
However this is not working on Hetzner. I have tried to use that and got timeout. Googling suggested several options what might be problem:
-
Ipv6 problem. I have even tried forcing ipv4 in dns resolution. No success.
-
Nodemailer not working properly in Docker. I have tested Docker hypothesis locally:
docker build -t ffff . docker run -p 3000:3000 -e MAIL_PASS=$MAIL_PASS ffff
-
Port blocking.
It was working properly from Docker, forcing ipv4 has not helped. So it was clear that problem is in Hetzner. Eventually I have found out that Hetzner blocks some ports https://docs.hetzner.com/cloud/servers/faq/#why-can-i-not-send-any-mails-from-my-server
The same article states that port 587 is OK so solution was:
let transporter = nodemailer.createTransport({
host: 'smtp.gmail.com',
port: 587,
secure: false,
requireTLS: true,
auth: {
user: process.env.MAIL_USER,
pass: process.env.MAIL_PASS,
},
});
Overall Kamal works for this service without problems as well.