When we want to deploy an ASP.NET Core app to the cloud, the usual candidate would be Azure. What if we want to deploy to all other PAAS providers that do not have native .NET Core support (e.g., Render)? Or an IAAS provider that has a Linux VM? Docker can come to the rescue in these scenarios.

In this blog post, we will build our ASP.NET Core app as a Docker container to publish in Docker Hub. Specifically, we will make sure the container is correctly targeting the ARM Ampere compute instance we are using in Oracle Cloud Infrastructure (OCI). We will then run it as a container image in the Oracle Linux VM on this ARM Compute instance.

I’ll assume that the reader has at least completed the first 4 steps in my previous post .

Setup Docker in Oracle VM

In this part, we will install Docker and setup network connectivity. We will configure new ingress rules in our VM subnet’s Security List, to open up ports for our Docker containers.

setup connectivity

Each VM is associated with a public subnet in a Virtual Cloud Network. Follow the steps to add a new ingress rule for this subnet. This will be used to map a port on the VM to the container.

  • Source CIDR: 0.0.0.0/0 (open to all clients)
  • IP Protocol: TCP
  • Source Port Range: Leave blank (all ports)
  • Destination Port Range: Enter whatever you like, in my case I enter 7777-7778
  • Click Add Ingress Rule to save

install Docker

We will now install Docker engine in our Oracle Linux VM. For Ubuntu, follow these offical steps .

  1. Ensure our system’s packages are up to date

    sudo dnf update -y
    
  2. Install necessary utilities and add the official Docker repository for RHEL (which is compatible with Oracle Linux)

    sudo dnf install -y yum-utils
    sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
    
  3. Install Docker Engine

    Now we can install Docker Engine, CLI, containerd , and Docker Compose from the newly added repository:

    sudo dnf install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
    
  4. Enable and start the Docker service

    sudo systemctl enable docker
    sudo systemctl start docker
    

If you want to start and enable the Docker service to run on boot, use sudo systemctl enable --now docker.

  1. Verify the Docker installation

    Check the Docker service status with sudo systemctl status docker.

    To confirm the installation, download a test image and run it in a container. A confirmation message indicates a successful installation.

    sudo docker run hello-world
    
  2. Manage Docker as a non-root user

    By default, only the root user can run Docker commands. To allow a regular user to manage Docker, add them to the docker group with sudo usermod -aG docker your_username, replacing your_username with the actual username.

    Or use sudo usermod -aG docker $USER to add the current user to the docker group.

    Log off and log back in for this change to take effect.

  3. To check network connectivity of Docker containers in our VM, we can validate with the default nginx container as it comes with a test page that can be accessed via our VM’s public IP.

    To do this, we will run it to listen on port 80, mapped to port 7778 in the VM. Remember 7778 is the Destination Port we have chosen in the previous section when setting up a new ingress rule. Substitute it for whatever port number you’ve chosen.

    docker run --name mynginx -p 7778:80 -d nginx
    

    Here’s a breakdown of the command:

    • docker run: Creates a new container and runs a command in it.
    • --name mynginx: Assigns a name to your container, in this case, “mynginx”. This makes it easier to refer to and manage the container later.
    • -p 7778:80: This option maps port 7778 on our host machine to port 80 inside the container. NGINX typically listens for web traffic on port 80.
    • -d: This flag runs the container in “detached” mode, meaning it runs in the background and doesn’t tie up our terminal.
    • nginx: Specifies the Docker image to use. In this case, it’s the official NGINX image from Docker Hub.

    After running the command, Docker will download the NGINX image (if it’s not found locally) and start a new container running it.

  4. Use docker ps to verify if the container is running.

  5. Open a browser and hit the VM’s public IP at port 7778, e.g., http://VM_PUBLIC_IP:7778. We should see NGINX’s welcome page.

Deploy ASP.NET Core app to Oracle VM

Now that we have prepared our VM, let’s build our image so we can deploy to our VM.

build & publish image

Prereq: Make sure Docker Desktop is installed. If you’re on Windows, you must enable WSL2 too.

Since I’m running the VM on the ARM Ampere Compute shape in OCI, I need to build a Docker image specifically for this platform.

In Visual Studio, you can use the Publish feature to publish to Docker Hub. VS will automatically compile the project and issue a Docker command to build and push an image. It works fine if your host architecture is the same as your target’s. For example, if your dev machine is x86 and you’re planning to deploy the container to an x86 platform.

However, it doesn’t work if the host/target arch are different. In my case, I assumed setting the Publish dialog’s Show All Settings/Target Runtime to linux/arm64 will take care of it. Instead, the built Docker image still defaults to linux/amd which will fail in our Oracle Ampere VM. This is because Target Runtime only sets what binaries VS compiles for, and has no effect on what architecture the container can run on.

In fact, VS will use the default docker builder which only builds for the host architecture (e.g., amd64 on most PCs). This can be verified in VS’s Output window, in which the following command is issued:

"C:\Program Files\Docker\Docker\resources\bin\docker.exe" build -f "<PROJECT PATH>\\Dockerfile" --force-rm -t projName --label "com.microsoft.created-by=visual-studio" --label "com.microsoft.visual-studio.project-name=projName"  --build-arg "BUILD_CONFIGURATION=Release" ""<PROJECT PATH>\\projName"

When I pull the image from my Oracle Ampere instance later, it gives a warning “The requested image’s platform (linux/amd64) does not match the detected host platform (linux/arm64/v8)”.

The correct command to build for a specific platform (arm64 in our case) is to skip VS Publish, and manually use the newer docker buildx with the --platform flag:

docker buildx build -f "<PROJECT PATH>\\Dockerfile" --platform linux/arm64 -t yourrepo/projName --push .
  • -f points to the Dockerfile (created automatically by Add Docker Support in VS Solution Explorer and choose a Target OS)
  • --platform linux/arm64 forces it to target this platform
  • -- push will push the image to Docker Hub when done

pull & run image

  1. In Oracle VM, verify the image’s supported platform

    Inspect its manifest list to ensure the build process is successful:

    docker manifest inspect yourrepo/projName
    

    Look for an entry with platform set to linux/arm64 or linux/arm64/v8. Or to jump right to the platform information, run

    docker image inspect yourrepo/projName --format '{{.Architecture}}'
    

    Expected value is arm64.

  2. Pull image from Docker Hub

    docker pull yourrepo/projName
    
  3. Discover which port the image listens to

    docker image inspect yourrepo/projName | grep -A2 ExposedPorts
    

    For example, I get

    "ExposedPorts": {
        "8080/tcp": {},
        "8081/tcp": {}
    

    This means the app listens on port 8080 and 8081 inside the container.

  4. Run the container

    Pick a host port you configured in the New Ingress Rule above (in my case, 7777) and map it to the container’s app port (replace <CONTAINER_PORT> with what you found in the previous step).

    docker run  -d --name projName \
    -p 7777:<CONTAINER_PORT> \
    -v /var/projName/data:/data \
    --restart unless-stopped \
    yourrepo/projName
    

    The -v flag is optional for specifying a volume to preserve data if the image supports persistence (e.g., configs or indexes).

    The --restart controlls the restart policy after a VM reboot. unless-stopped will make sure it will restart after a crash or VM reboot, but not after you manually stop the container.

    We can also feed environment variables to the image with -e KEY=value or batch add with a file using --env-file /path/to/file. When the container is up and running, we can check if the environment variable is passed correctly to the container by listing all of them: docker exec <container_id_or_name> env

  5. Now we can hit our ASP.NET Core app in the container by http://VM_PUBLIC_IP:7777!

Final thoughts

In this post, we have taken an ASP.NET Core project and manually build a Docker container targeting Linux on ARM64 architecture. We have setup the right connectivity options in an Oracle ARM-based Ampere instance. Finally, we’ve deployed the container as an image and accessed it successfully from public.

For the container to work in an architecture different from your dev machine, the only trick is to ensure we use the newer buildx Docker builder which supports building for specific and multiple architectures.