DevOps

Deploying Nuxt 3 with Kamal 2 on Docker: Statically or with SSR

Profil Picture

Guillaume Briday

5 minutes

I often deploy static websites and have experience with various tools like Nuxt, Next, Jekyll, and Gatsby.

Platforms like Netlify, Vercel, and Cloudflare make it easy to deploy a Nuxt app. Personally, I’m a big fan of Netlify because it is both reliable and simple to use. However, it is important to keep an eye on their pricing and terms to avoid surprises.

In fact, there have been some cases where people received unexpectedly high bills, sometimes reaching hundreds of thousands of dollars, for what they thought were basic projects.

I had a similar experience with Netlify, where I assumed my project would stay within the free tier, but then received a bill for Stimulus Components. I had exceeded the free-tier limits without any way to block traffic. You can read more about that here.

To avoid this headache, we’ll use Kamal. Kamal makes the process easier by providing scripts that run Docker commands over SSH. Although it was originally built for Rails apps, Kamal works for any app that can be containerized, including our Nuxt app.

In this blog post, I will guide you through deploying a Node app with Nuxt 3 as our example, though these steps work for almost any Node-based or static site generator, such as Nuxt, Next, and Jekyll.

With Nuxt, there are two main deployment options. You can deploy it to a Node.js server with SSR or set it up as a pre-rendered static site. We will cover both options here. For SSR, you will only need Node, while for static hosting, you will also need Nginx to serve the static files.

In this guide, we will go over how to:

  • Install Kamal 2 in a Nuxt project
  • Set up your DNS
  • Create a Dockerfile (Node-only for SSR, or with Nginx for static hosting)
  • Set up Kamal-proxy and Nginx to automatically manage SSL certificates
  • Configure Nginx
  • Deploy the application

Let’s start with the initial setup and configuration!

1. Install Kamal 2

Kamal comes as a Ruby Gem:

$ gem install kamal

After installation, verify that it's correctly installed by checking the version:

$ kamal version

For more info, see: https://kamal-deploy.org/docs/installation/.

2. Prepare your DNS

It's important to configure your DNS early, as it may take some time to propagate. Kamal-proxy will also need the DNS configuration to generate the SSL certificate.

Here’s an example of how to configure the DNS:

my-awesome-app 86400 IN A 192.168.0.1 ; Configured as a subdomain.

; OR

@ 86400 IN A 192.168.0.1 ; Configured as a top-level domain.

Make sure to replace 192.168.0.1 with the actual IP address of your server.

3. Prepare your server

With Kamal, everything happen in Docker, so theoretically you don't anything more installed on your server. Also, Kamal will automatically install Docker for you.

But if you have just created a new server, you probably want to configure basic security rules and better default config.

To automate these actions, I created an Ansible playbook so you don't have to run every command manually, it's especially handy if you deploy your app on multiple servers with load balancing.

You can follow the instructions here if you are interested: 👉 Kamal Ansible Manager

Basically, it will upgrade already installed packages and install the UFW Firewall, Fail2ban and NTP.

4. Prepare the Configuration Files

Start by generating the required configuration files for Kamal with the following command:

$ kamal init

This command creates two key files: .kamal/secrets and config/deploy.yml.

  • .kamal/secrets: Define your secret environment variables here, which will be securely pushed to your servers.
  • config/deploy.yml: Contains the main Kamal configuration settings.

Note: If you’re using Docker Hub, ensure the KAMAL_REGISTRY_PASSWORD in your .env file is an access token, not your actual password. For details, check out the Docker access tokens guide.

4.1. For Pre-rendered Static Sites (e.g., Netlify)

In config/deploy.yml, configure your deployment settings as shown below:

<% require "dotenv"; Dotenv.load(".env") %>

# Name of your application. Used to uniquely configure containers.
service: my_awesome_app

# Name of the container image.
image: your-name/my_awesome_app

# Deploy to these servers.
servers:
  web:
    - 192.168.0.1

proxy:
  app_port: 80 # Nginx exposes port 80 by default
  ssl: true
  host: my_awesome_app.com
  healthcheck:
    path: / # Default is /up, adjust if needed

# Credentials for your image host.
registry:
  username: your-name
  password:
    - KAMAL_REGISTRY_PASSWORD # Access token pulled from .kamal/secrets

builder:
  arch: amd64

4.2. Prepare your Dockerfile

Below is a sample Dockerfile for a Nuxt project. Feel free to adjust based on your project’s needs:

FROM node:20-alpine AS build

WORKDIR /app

COPY . ./

RUN corepack enable
RUN yarn install --immutable
RUN yarn run generate

# Use Nginx to serve static files
FROM nginx:alpine-slim

COPY ./config/nginx/default.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/docs/.output/public /usr/share/nginx/html

In this example, the default Nginx configuration is customized to add caching for assets, which can improve performance. For more information, see the Nginx Docker Hub page.

Custom Nginx Configuration

Below is the full default.conf I use. You can adapt it to fit your project:

View the complete default.conf file:
server {
    listen       80;
    listen       [::]:80;
    server_name  localhost;

    root   /usr/share/nginx/html;
    index  index.html;

    # Suppress logs for common paths
    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt { access_log off; log_not_found off; }
    location /_nuxt/ { access_log off; log_not_found off; }
    location ~* _payload\.json$ { access_log off; log_not_found off; }

    # Cache static assets (images, CSS, JS, fonts)
    location ~* \.(css|js|jpe?g|png|gif|ico|cur|heic|webp|tiff?|mp3|m4a|aac|ogg|midi?|wav|mp4|mov|webm|mpe?g|avi|ogv|flv|wmv)$ {
        add_header Cache-Control "public, max-age=31536000, immutable";
        access_log off;
        log_not_found off;
    }

    location ~* \.(svgz?|ttf|ttc|otf|eot|woff2?)$ {
        add_header Cache-Control "public, max-age=31536000, immutable";
        add_header Access-Control-Allow-Origin "*";
        access_log off;
    }

    # Enable gzip compression
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
}

With Kamal, the build, push, and pull processes for your Docker image are automated, requiring only your registry credentials.

⚠️ You won’t need to build and publish the image manually. Kamal takes care of it!

4.3. For SSR applications

With SSR, there’s no need to use Nginx. The Node application will handle serving all content and assets on its own.

In your config/deploy.yml, configure the deployment settings like this:

<% require "dotenv"; Dotenv.load(".env") %>

# Name of your application. Used to uniquely configure containers.
service: my_awesome_app

# Name of the container image.
image: your-name/my_awesome_app

# Deploy to these servers.
servers:
  web:
    - 192.168.0.1

proxy:
  app_port: 3000 # Nuxt defaults to exposing port 3000
  ssl: true
  host: my_awesome_app.com
  healthcheck:
    path: / # The default is /up, but customize if necessary

# Credentials for your image host.
registry:
  username: your-name
  password:
    - KAMAL_REGISTRY_PASSWORD # Access token from .kamal/secrets

builder:
  arch: amd64

4.4. Prepare your Dockerfile

Here’s an example Dockerfile for an SSR Nuxt project. Adjust it to meet any specific requirements:

FROM node:20-alpine AS build

WORKDIR /app

COPY . ./

RUN corepack enable
RUN yarn install --immutable
RUN yarn run build

# Serve the app directly from Node
FROM build

# Copy the entire .output folder
COPY --from=build /app/docs/.output /app

EXPOSE 3000
CMD ["node", "/app/server/index.mjs"]

For further details, you can refer to the Nuxt deployment documentation.

7. First deployment

We're ready to deploy the app! But before the first deployment, let’s set up Kamal:

$ kamal setup

This command will handle several important tasks:

  • Install Docker if it isn’t already on your server.
  • Start and configure the kamal-proxy container.
  • Load all environment variables from your .kamal/secrets file.
  • Build and push your Docker image to the registry.
  • Launch a new container with the latest version of your app.

8. Workflow

With everything now running smoothly, here are some essential commands you’ll use frequently:

To deploy a new version of your app:

$ kamal deploy

To view your application logs:

$ kamal app logs

9. Conclusion

It's easy as that, enjoy your reliable and cheap deploy! 🚀

Simplify your time tracking with Timecop

Timecop is a time tracking app that brings simplicity in your day to day life.

Timecop projects