Deploy SvelteKit App in Docker Container [Behind nginx reverse proxy]

We all use Docker everyday to run our applications in containers. It's a great tool that helps us to package our applications and run them in an isolated environment. In this post, I'll share how I deploy my SvelteKit apps in a Docker container behind the nginx reverse proxy.

Prerequisites

Before we start, make sure you have the following installed on your machine:

  • Node.js - Version 20 or higher
  • Docker with Docker compose plugin
  • If you wish to deploy the app in production environment, you need a VPS server with root access and public IP address attached to it.

If you do not have the VPS server yet, you can use the DigitalOcean or Linode to create a VPS server. Both the providers offer free credit to try their services.

I'll assume that you already have a SvelteKit app ready to deploy. If you don't have one, you can create a new SvelteKit app as described by the SvelteKit documentation.

Following is the script section of the package.json file of the SvelteKit app:

{
	"name": "app.domain.com",
	"description": "A demo SvelteKit app",
	"version": "1.0.0",
	"type": "module",
	"scripts": {
		"start": "ORIGIN=https://app.domain.com PORT=4800 NODE_ENV=production node --require dotenv/config build",
		"dev": "vite dev --mode development",
		"build": "vite build",
		"preview": "cross-env NODE_ENV=production vite preview",
		"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
		"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
		"lint": "eslint ./src/"
	}
}

1. Install Node Adapter for SvelteKit

To deploy the SvelteKit app in a Node environment, we need to install the @sveltejs/adapter-node package. Install the required dev dependency by running the following command:

npm install --save-dev @sveltejs/adapter-node

@sveltejs/adapter-node generates an app that can be run in a Node environment. For more information about the adapter, you can check the official documentation.

Once the package is installed your package.json file should look like this:

"devDependencies": {
	"@sveltejs/adapter-auto": "^3.1.0",
	"@sveltejs/adapter-node": "^5.2.11",
	"@sveltejs/kit": "^2.15.0",
	"@sveltejs/vite-plugin-svelte": "^3.0.1",
	"@types/node": "^20.10.8",
	"@typescript-eslint/eslint-plugin": "^6.18.1",
	"@typescript-eslint/parser": "^6.18.1",
	"svelte": "^4.2.19",
	"svelte-check": "^3.8.6",
	"tailwindcss": "^3.4.17",
	"tslib": "^2.8.1",
	"typescript": "^5.7.2",
	"vite": "^5.1.8",
}

2. SvelteKit Configuration

While creating the SvelteKit app using the npx sv command adapter auto is installed by default. SvelteKit app inside the Docker container runs in a Node environment. Before creating the production build of the app, we need to update the svelte.config.js file to set the kit.adapter to @sveltejs/adapter-node explicitly.

// svelte.config.js

import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

/**
* SvelteKit configuration
*
* @ref https://svelte.dev/docs/kit/configuration
* @type {import('@sveltejs/kit').Config}
* @since 1.0.0
*/
const config = {
	extensions: ['.svelte', '.md'],
	preprocess: [vitePreprocess()],
	kit: {
		adapter: adapter({ out: 'build' }),
		env: {
			dir: "./"
		},
		alias: {
			$src: "./src",
		}
	},
};

export default config;

The is the only change required in the SvelteKit app config to generate the app that can be deployed later using the Docker container.

3. Create Dockerfile

Once the node adapter is installed and the SvelteKit app is configured, create a Dockefile in the root of the project. The Dockerfile contains the instructions to build the Docker image.

FROM oven/bun:alpine AS builder

WORKDIR /tmp/app

COPY bun.* ./
COPY package*.json ./

RUN bun install

COPY . .

FROM node:20-alpine AS runners

WORKDIR /app

COPY --from=builder /tmp/app/build ./build
COPY --from=builder /tmp/app/package*.json ./
COPY --from=builder /tmp/app/node_modules ./node_modules

ENV PORT=4800
ENV NODE_ENV=production

# Expose the port the app runs on
# The app runs on http://localhost:4800
EXPOSE 4800

# Server the app.
ENTRYPOINT ["npm", "start"]

I am using Docker multi-stage build to create the Docker image. The first stage is the builder stage where the SvelteKit app is built using the Bun JS. The second stage is the runners stage where the app is served using the npm start command using the Node environment.

Note: You can use Node JS in both the stages to build and serve the app. If you wish to run the app in a different port, you can update the port environment variable in the Dockerfile and expose the port accordingly.

4. Docker Ignore File

Create a .dockerignore file in the root of the project to ignore the files that are not required in the Docker image.

/docs
/node_modules
/invoices

Docker ignore file helps to reduce the size of the Docker image by ignoring the files that are not required in the image.

5. Create a Docker Network

This step is optional. If you are running multiple services in Docker and want to communicate between them, you can create a Docker network. To create a Docker network use the following command:

docker network create sveltekit-app

If you don't want to create the network, you can remove the networks section from the docker-compose.yml file later.

6. Docker Compose File

Create a docker-compose.yml file in the root of the project to run the Docker container. The docker-compose.yml file contains the configuration to run the Docker container.

services:
  sveltekit-app:
    image: sveltekit-app:latest
    container_name: sveltekit-app
    ports:
      - "4800:4800"
    restart: always
    volumes:
      - ./data:/app
    networks:
      - sveltekit-app
    env_file:
      - .env
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  sveltekit-app-2:
    image: sveltekit-app:latest
    container_name: sveltekit-app-2
    ports:
      - "4801:4800"
    restart: always
    volumes:
      - ./data:/app
    networks:
      - sveltekit-app
    env_file:
      - .env
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

volumes:
  data:

networks:
  sveltekit-app:
    external: true

The network section is optional. If you have created a network in the previous step, you can use it here. If you don't want to use the network, you can remove the networks section from the docker-compose.yml file. The logging option in the docker-compose.yml file is optional. You can remove it if you don't want to limit the log size of the container.

In the above docker-compose.yml file, I have created two services sveltekit-app and sveltekit-app-2. Both the services run the same SvelteKit app but on different ports. You can run multiple instances of the same app using the same Docker image and perform the load balancing using the nginx reverse proxy. I'll later guide you how you can set up the nginx reverse proxy to load balance the requests between the multiple instances of the SvelteKit app.

Note: If you are copying the docker-compose.yml file from this post, make sure to fix the indentation of the file. The indentation is important in the docker-compose.yml file.

7. Build the Image and Run the Docker Container

Once the Dockerfile and docker-compose.yml files are created in the root of the project, build the Docker image using the following command:

docker build -t sveltekit-app .

If everything goes well, the Docker image for the SvelteKit app will be created using the multi-step build steps defined in a Dockerfile.

Now, run the following command to check if the Docker image was created successfully:

docker images

The output of the above command should show the sveltekit-app image in the list of Docker images.

7.1 Run the Docker Container

Running the container is simple. Run the following command to run the Docker container:

docker-compose up -d

The above command will use the docker-compose.yml file to run the Docker container. The -d flag is used to run the container in the detached mode. The container will run in the background.

To check if the container is running, run the following command:

docker ps

The output of the above command should show the running container in the list of Docker containers. If the app crashed you can view the logs using the following command:

docker logs sveltekit-app

If the app is running in the Docker container, you can access the app in the browser using the following URL:

http://localhost:4800

Note: Port 4800 was defined in the start command of the package.json file. If you have changed the port in the package.json file, you can access the app accordingly.

If you wish to access the app in your public IP address you might need to allow the port in the firewall settings of your server.

8. Configure nginx

I'll assume you have a basic understanding of the nginx web server. If you do not have nginx installed on your server, now it's time to install it. I'll skip the installation part of nginx in this post.

In this step we need to do few things:

  • Setup DNS A record to point the domain name to the server IP address
  • Have nginx web server installed on the server. You can verify the status of ngins by running the following command:
nginx -v

If the above command returns the nginx version, it means nginx is installed on the server.

8.1 Setup DNS A Record

Before configuring the nginx reverse proxy, make sure you have a domain name pointing to your server IP address. Create an A record in your DNS provider to point the domain name to your server IP address.

8.2 Create a nginx Configuration File

Create a new configuration file in the /etc/nginx/sites-available/ directory. The name of the configuration file should be app.domain.com. The configuration file contains the configuration to load balance the requests between the multiple instances of the SvelteKit app.

sudo nano /etc/nginx/sites-available/app.domain.com

At this point, you might wish to install the SSL certificate for the domain name. I'll assume you are aware of certbot and how to install the SSL certificate for the domain name in nginx server.

Once the certificate is installed, add the following configuration to the nginx configuration file:

# Define the upstream load-balancing group
upstream sveltekit-app {
	# Docker container instances
	server 127.0.0.1:4800;
	server 127.0.0.1:4801;
}

# Redirect all HTTP traffic to HTTPS
server {
	listen 80;
	server_name app.domain.com;
	return 301 https://$host$request_uri;
}

# Main HTTPS server block
server {
	listen 443 ssl;
	server_name app.domain.com;

	# SSL Configuration
	ssl_certificate    /www/certs/app.domain.com/fullchain.pem; # Update this path to your SSL certificate.
	ssl_certificate_key    /www/certs/app.domain.com/privkey.pem; # Update this path to your SSL certificate key.
	ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
	ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
	ssl_prefer_server_ciphers on;
	ssl_session_cache shared:SSL:10m;
	ssl_session_timeout 10m;

	# HSTS (HTTP Strict Transport Security)
	add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

	error_page 497 https://$host$request_uri;

	# Proxy settings for load balancing
	location / {
		proxy_pass http://sveltekit-app;

		# Load balancing configurations
		proxy_redirect off;
		proxy_set_header Upgrade $http_upgrade;
		proxy_set_header Connection 'upgrade';
		proxy_set_header Host $host;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_set_header X-Forwarded-Proto $scheme;
		proxy_set_header X-Forwarded-Host $host;
		proxy_set_header X-Forwarded-Port $server_port;
		proxy_set_header X-Forwarded-Prefix /;
		proxy_set_header X-Original-URI $request_uri;
		proxy_set_header CF-Connecting-IP $http_cf_connecting_ip;

		# Timeout settings
		proxy_connect_timeout 60s;
		proxy_send_timeout 60s;
		proxy_read_timeout 60s;
		send_timeout 60s;
	}

	# Directory verification settings for SSL certificate application
	location ~ \.well-known {
			allow all;
	}

	# Prohibit sensitive files in verification directory
	if ($uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$") {
			return 403;
	}

	# Logs
	access_log /www/logs/app.domain.com.log; # Update this path to your access log file.
	error_log /www/logs/app.domain.com.error.log; # Update this path to your error log file.
}

In the above configuration, I have defined the upstream load-balancing group sveltekit-app that contains the two Docker container instances running the SvelteKit app. The proxy_pass directive is used to load balance the requests between the two instances of the SvelteKit app. The upstream group can contain multiple instances of the SvelteKit app. You can add more instances to the upstream group to scale the app horizontally.

Double check the port numbers in the upstream group and the proxy_pass directive. The port numbers should match the port numbers defined in the docker-compose.yml file. To verify you can always check the running Docker containers using the docker ps command.

8.2.1 nginx Configuration with no SSL Certificate

If you do not have the SSL certificate installed for the domain name, or you do not wish to use the SSL certificate, you can use the following configuration in the nginx configuration file:

# Define the upstream load-balancing group
upstream sveltekit-app {
  # Docker container instances
  server 127.0.0.1:4800;
  server 127.0.0.1:4801;
}

# Main HTTP server block
server {
	listen 80;
	server_name app.domain.com;

	# Proxy settings for load balancing
	location / {
		proxy_pass http://sveltekit-app;

		# Load balancing configurations
		proxy_redirect off;
		proxy_set_header Upgrade $http_upgrade;
		proxy_set_header Connection 'upgrade';
		proxy_set_header Host $host;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_set_header X-Forwarded-Host $host;
		proxy_set_header X-Forwarded-Port $server_port;
		proxy_set_header X-Forwarded-Prefix /;
		proxy_set_header X-Original-URI $request_uri;
		proxy_set_header CF-Connecting-IP $http_cf_connecting_ip;

		# Timeout settings
		proxy_connect_timeout 60s;
		proxy_send_timeout 60s;
		proxy_read_timeout 60s;
		send_timeout 60s;
	}

	# Directory verification settings (if needed for other purposes)
	location ~ \.well-known {
		allow all;
	}

	# Prohibit sensitive files in verification directory
	if ($uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$") {
		return 403;
	}

	# Logs
	access_log /www/logs/app.domain.com.log;    # Update this path to your access log file
	error_log /www/logs/app.domain.com.error.log;  # Update this path to your error log file
}

In the above configuration, I have removed the SSL configuration and added the main HTTP server block to load balance the requests between the multiple instances of the SvelteKit app. In this case, nginx server listens on port 80 and forwards the requests to the upstream group.

8.3 Enable the nginx Configuration

Once the configuration file is created, enable the configuration file by creating a symbolic link in the /etc/nginx/sites-enabled directory. Run the following command to create the symbolic link:

sudo ln -s /etc/nginx/sites-available/app.domain.com /etc/nginx/sites-enabled/app.domain.com

8.4 Verify the nginx Configuration

Before restarting the nginx server, verify the configuration file using the following command:

sudo nginx -t

If the configuration file is correct, you should see the following output:

nginx: configuration file /etc/nginx/nginx.conf test is successful

8.5 Restart nginx Server

Once the configuration file is verified, restart the nginx server using the following command:

sudo systemctl restart nginx

Verify the status of the nginx server using the following command:

sudo systemctl status nginx

If the nginx server is running, you can access the SvelteKit app using the domain name https://app.domain.com in the browser.

Conclusion

In this post, I shared how I deploy my SvelteKit apps in a Docker container behind the nginx reverse proxy. I hope you find this post helpful. The version of Svelte framework used in this post is 4.2.19.

I am aware that Svelte recently released version 5 with some breaking changes. I haven't tested the Svelte 5 yet but I am confident that the steps mentioned in this post will work with the Svelte 5 version as well.

If you found this post helpful, please share it with others. If you have any questions or feedback, feel free to drop me a line.