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}
}