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! 🎉).
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
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} }