Migrating a web application from Windows to Ubuntu

A few weeks ago I decided to upgrade the logbee.net server. With this opportunity, I also made the transition from Windows to a Linux ecosystem. While I had some experience with Linux, I had never set up a production-ready Linux server from scratch.

This blog post walks through my experience of moving logbee.net to Ubuntu 22.04 LTS, migrating from IIS to a fully containerized infrastructure, using Docker and Caddy as the new key components of the new setup. Surprisingly, the process was easier than I anticipated 🚀

Setting up the Ubuntu server

Step 1. Preparing the server

The first thing I did was to provision a fresh Ubuntu 24.04 LTS machine on my cloud provider.

Once the server was ready, I received the SSH login credentials on my email, so I tried to connect to it from my Windows terminal.

C:\Users\catalin.gavan>ssh root@148.234.162.100
root@148.234.162.100's password:

After the successful login, I got access to the Ubuntu server (awesome! 🎉).

ssh login

Before installing any new packages, I ensured the system is up to date.

sudo apt update
sudo apt upgrade

TIP: It is a good practice to create a non-root user for security reasons.

Step 2. Installing Docker

Since my application runs in Docker containers, the next step was to install Docker and Docker Compose.

# installs Docker Engine and Docker Compose
sudo apt install -y docker.io docker-compose

#  ensures the Docker engine starts automatically at system boot
sudo systemctl enable --now docker

At this point, the new server is ready to host any Docker application.

Setting up logbee.net

I created a new folder where I will keep all the configuration for the Logbee application.

mkdir -p ~/logbee/logbee-app

Inside this directory, I created the docker-compose.yml file and the other necessary configuration files. Here is a breakdown of the files:

~/logbee/logbee-app/
   ├─ caddy/
   │  ├─ Caddyfile            # Configuration for Caddy reverse proxy
   │  └─ Dockerfile           # Custom build for Caddy with rate limiting
   ├─ docker-compose.yml      # Defines all the application services	
   ├─ backend.logbee.json     # Configuration for the Logbee REST service
   ├─ frontend.logbee.json    # Configuration for the Logbee Web UI
   └─ mariadb.cnf             # Custom MariaDB settings

mariadb.cnf

Since Linux treats file paths as case-sensitive and MySQL (MariaDB) relies on the filesystem to manage data, I had to explicitly configure MySQL to use lowercase table names.

[mysqld]
lower_case_table_names=1

caddy/Caddyfile

I used Caddy to replace the basic functionality that I previously configured in IIS on Windows. Here is what Caddy does for my application:

  • maps the host name to the server IP address
  • configures rate limiting to avoid excessive requests
  • implements static file caching for better performance

The Caddyfile looks like following:

logbee.net, www.logbee.net {
    reverse_proxy logbee.frontend:80

    @staticFiles {
      path /dist/*
      path /img/*
      path /cdn/*
      path /data/*
   }

   header @staticFiles Cache-Control "public, max-age=2592000, immutable"
}

api.logbee.net {
    reverse_proxy logbee.backend:80

    rate_limit {
       zone target_per_ip {
          key target_per_ip-{client_ip}-{http.request.host}
          events 20
          window 1s
       }
       log_key
    }
}

http://logbee.net, http://api.logbee.net {
    redir https://{host}{uri}
}

docker-compose.yml (simplified)

version: "3.7"

networks:
  default:
    name: logbee-net
    driver: bridge
    driver_opts:
      com.docker.network.driver.mtu: 1380

services:
  backend:
    image: catalingavan/logbee.backend:2.0.0
    container_name: logbee.backend
    restart: unless-stopped
    ports:
      - "44088:80"
    depends_on:
      - mongodb
      - mariadb
    networks:
      - default

  frontend:
    image: catalingavan/logbee.frontend:2.0.0
    container_name: logbee.frontend
    restart: unless-stopped
    ports:
      - "44080:80"
    depends_on:
      - mongodb
      - mariadb
      - backend
    networks:
      - default

  mongodb:
    image: mongo:8.0.4
    container_name: logbee.mongodb
    restart: unless-stopped
    networks:
      - default

  mariadb:
    image: mariadb:11.7
    container_name: logbee.mariadb
    restart: unless-stopped
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
    volumes:
      - ./mariadb.cnf:/etc/mysql/conf.d/mariadb.cnf
    networks:
      - default

  caddy:
    build: caddy
    container_name: logbee.caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./caddy/Caddyfile:/etc/caddy/Caddyfile
    networks:
      - default

Running the application

Once all the files were updated and configured, I was ready to start the application:

sudo docker-compose up -d

To confirm everything was running correctly, I checked the status with:

sudo docker-compose ps

docker-compose ps

Cheatsheet

Throughout this process, I picked up a few tips that saved me time troubleshooting issues.

Upload files from Windows to Ubuntu

Editing large files in a terminal can be inconvenient, especially if you're used to a Windows environment.

To solve this, I used the scp command from PowerShell, which can be used to upload files from a Windows computer to a Linux computer.

This way, I could use Visual Studio Code to edit the files on my personal computer, making this easier for editing.

scp -r .\logbee-app root@148.234.162.100:~/logbee/logbee-app

Useful Docker commands

Force rebuild all the docker-compose services:

docker-compose up -d --force-recreate

Restart a service:

docker-compose restart caddy # [service_name]

Read the logs for a container:

docker logs logbee.frontend -f
docker logs logbee.frontend --tail 50

Attach to a container terminal. This is useful if you want to check what is inside a container, or to check if the mounted volumes are working correctly.

docker exec -it <containerName> [bash|sh]
sudo docker exec -it logbee.frontend sh

Conclusion

More than 14 days have passed since the successful migration, and everything is running without any issues. The transition was easier than I initially anticipated, and once you've configured Docker and Caddy, the majority of the work is already done.

Optionally, you can install a VPC service (such as Tailscale) if you want to securely access to your MongoDB or MariaDB data from outside your Docker containers.

All files

docker-compose.yml

version: "3.7"

networks:
  default:
    name: logbee-net
    driver: bridge
    driver_opts:
      com.docker.network.driver.mtu: 1380

services:
  backend:
    image: catalingavan/logbee.backend:2.0.0
    container_name: logbee.backend
    restart: unless-stopped
    environment:
      - ASPNETCORE_URLS=http://0.0.0.0:80
      - LOGBEE_BACKEND_CONFIGURATION_FILE_PATH=Configuration/backend.logbee.json
    volumes:
      - ./logbee.backend/logbee.json:/app/Configuration/backend.logbee.json
      - ./logbee.frontend/logbee.json:/app/Configuration/frontend.logbee.json
      - ../docker-volumes/backend/logs:/app/app/logs
    ports:
      - "44088:80"
    depends_on:
      - mongodb
      - mariadb
    networks:
      - default

  frontend:
    image: catalingavan/logbee.frontend:2.0.0
    container_name: logbee.frontend
    restart: unless-stopped
    environment:
      - ASPNETCORE_URLS=http://0.0.0.0:80
      - LOGBEE_FRONTEND_CONFIGURATION_FILE_PATH=Configuration/frontend.logbee.json
    volumes:
      - ./logbee.backend/logbee.json:/app/Configuration/backend.logbee.json
      - ./logbee.frontend/logbee.json:/app/Configuration/frontend.logbee.json
      - ../docker-volumes/frontend/logs:/app/app/logs
      - ../docker-volumes/frontend/DataProtection-Keys:/root/.aspnet/DataProtection-Keys
    ports:
      - "44080:80"
    depends_on:
      - mongodb
      - mariadb
      - backend
    networks:
      - default

  mongodb:
    image: mongo:8.0.4
    container_name: logbee.mongodb
    restart: unless-stopped
    volumes:
      - ../docker-volumes/mongo/data-db:/data/db
      - ../docker-volumes/mongo/data-configdb:/data/configdb
    networks:
      - default

  mariadb:
    image: mariadb:11.7
    container_name: logbee.mariadb
    restart: unless-stopped
    environment:
      - MYSQL_ROOT_PASSWORD=Your(Strong!)Password
    volumes:
      - ./mariadb.cnf:/etc/mysql/conf.d/mariadb.cnf
      - ../docker-volumes/maria/mysql:/var/lib/mysql
    networks:
      - default

  caddy:
    build: caddy
    container_name: logbee.caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./caddy/Caddyfile:/etc/caddy/Caddyfile
      - ../docker-volumes/caddy_data:/data
      - ../docker-volumes/caddy_config:/config
    networks:
      - default

volumes: {}

backend.logbee.json (simplified)

{
  "LogbeeFrontendConfigurationFilePath": "Configuration/frontend.logbee.json",
  "LogbeeBackendUrl": "https://api.logbee.net",
  "Database": {
    "Provider": "MongoDb",
    "MongoDb": {
      "ConnectionString": "mongodb://logbee.mongodb:27017?socketTimeoutMS=5000&connectTimeoutMS=5000",
      "DatabaseName": "LogbeeBackend"
    }
  },
  "FileStorage": {
    "Provider": "MongoDb",
    "MaximumFileSizeInBytes": 2097152,
    "MongoDb": {
      "ConnectionString": "mongodb://logbee.mongodb:27017?socketTimeoutMS=5000&connectTimeoutMS=5000",
      "DatabaseName": "LogbeeBackend"
    }
  }
}

frontend.logbee.json (simplified)

{
  "LogbeeBackendConfigurationFilePath": "Configuration/backend.logbee.json",
  "LogbeeFrontendDomain": "logbee.net",
  "StaticResourcesVersion": "2.0.0",
  "LogbeeFrontendUrl": "https://logbee.net",
  "Database": {
    "Provider": "MySql",
    "MySql": {
      "ConnectionString": "server=logbee.mariadb;port=3306;database=logbeefrontend;uid=root;password=Your(Strong!)Password;Charset=utf8;"
    }
  }
}

mariadb.cnf

[mysqld]
lower_case_table_names=1

caddy/Dockerfile

FROM caddy:2-builder AS builder

RUN xcaddy build \
    --with github.com/mholt/caddy-ratelimit

FROM caddy:2

COPY --from=builder /usr/bin/caddy /usr/bin/caddy

caddy/Caddyfile

logbee.net, www.logbee.net {
    reverse_proxy logbee.frontend:80

    @staticFiles {
      path /dist/*
      path /img/*
      path /cdn/*
      path /data/*
   }

   header @staticFiles Cache-Control "public, max-age=2592000, immutable"
}

api.logbee.net {
    reverse_proxy logbee.backend:80

    rate_limit {
       zone target_per_ip {
          key target_per_ip-{client_ip}-{http.request.host}
          events 20
          window 1s
       }
       log_key
    }
}

http://logbee.net, http://api.logbee.net {
    redir https://{host}{uri}
}