Manage Docker Containers from Your iPad

Deploy Portainer on Proxmox and manage Docker containers from your iPad.

Introduction

30 Second Summary

Managing servers usually means sitting at a desk with a terminal open. What if you could spin up web apps, databases, and services from your iPad while sitting on the couch?

In this project, you will install Docker Engine on a Proxmox VM and deploy Portainer as a visual management dashboard. You will also set up Tailscale so you can control your containers from your iPad anywhere in the world.

What You'll Build

A complete Docker management environment running on your Proxmox server, with a Portainer web dashboard accessible from your iPad through a Tailscale mesh VPN.

By the end of this project, you'll have:

  • A dedicated Docker host VM with Docker Engine and Portainer CE running on your Proxmox server.
  • Hands-on experience pulling images, creating containers, and deploying a full WordPress + MySQL stack using Docker Compose from your iPad's browser.
  • Remote access to your Docker dashboard from anywhere in the world using a Tailscale mesh VPN.
  • In the Secret Mission, you'll deploy a Portainer Agent on a second Docker host and manage both from a single dashboard.

Are there any prerequisites?

This project picks up where the "Install Proxmox and Build a Linux VM" project left off. You need Proxmox VE running on a PC with a working Ubuntu 24.04 VM template (ID 9000), an SSH key pair configured on your Mac, and an iPad on the same network.

Clone Your Template and Create a Docker Host VM

In the previous project, you installed Proxmox VE and created an Ubuntu 24.04 VM template (ID 9000). That template is your reusable starting point for every new server you build.

In this step, you'll clone that template to create a dedicated VM for running Docker. The template stays untouched. You get a fresh, independent copy ready to configure.

In this step, get ready to:
  • Clone your Ubuntu 24.04 VM template to create a Docker host VM.
  • SSH into the new VM and update its packages.
Clone VM 9000 to create a new VM

Your VM template (ID 9000) is like a master copy. Cloning it creates a new, independent VM with the same operating system and configuration already baked in.

  • Open a browser on your Mac and navigate to your Proxmox web UI. The URL is https:// followed by your Proxmox server's IP address and port :8006.
  • Log in with your Proxmox credentials.
  • In the left sidebar, find VM 9000 (your Ubuntu template).
  • Right-click on VM 9000 and select Clone.

The Clone: Virtual Machine dialog appears with several fields to configure.

  • Set VM ID to 200.
  • Set Name to docker-host.
  • Set Mode to Full Clone.
  • Click Clone.

What is a full clone?

A full clone creates a complete, independent copy of the template's disk. The new VM has no dependency on the original template.

A linked clone would share the template's disk to save space, but if the template were ever deleted, the linked clone would break. Full clones are safer for long-lived VMs like your Docker host.

Proxmox copies the disk. You'll see a lock icon next to the new VM while this runs. Wait for the cloning task to finish before continuing.

  • Click on VM 200 (docker-host) in the left sidebar.
  • Click the Cloud-Init tab in the center panel.
  • Click Regenerate Image to apply the new VM name as the hostname.

Why regenerate the image?

Cloud-Init uses the VM name as the hostname inside the guest operating system. Regenerating the image updates the Cloud-Init configuration so the VM boots with the hostname docker-host instead of the old template name.

  • Click Start in the top-right toolbar to boot the VM.

VM 200 should now show a status of running in the left sidebar.

SSH into the new VM and update the system

Cloud-Init needs about a minute to finish configuring the VM on its first boot. After that, you can connect via SSH.

  • In the Proxmox web UI, click on VM 200 and open the Summary tab.
  • Find the VM's IP address in the IPs section. Write it down.

Pro tip: no IP showing?

If the IP address field is empty, wait another minute. Cloud-Init may still be configuring the network. You can also check the Console tab and log in directly to run ip a to find the address.

  • Open Terminal on your Mac by pressing Cmd+Space and typing Terminal, then pressing Enter.
  • SSH into the new VM by running this command:
ssh ubuntu@[[VM_IP="your VM's IP address"]]

Connection refused or timed out?

  • Double-check the IP address matches what the Proxmox Summary tab shows.
  • If you see Permission denied (publickey), make sure you're using the same Mac where your SSH key pair (~/.ssh/id_ed25519) was created in the Proxmox project.
  • If the VM just booted, wait another 30 seconds for Cloud-Init to finish and try again.
  • help me debug my SSH connection to the Docker host VM

You should see a welcome message and an ubuntu@docker-host prompt confirming you're connected.

  • Update all system packages by running this command:
sudo apt update && sudo apt upgrade -y

What does this command do?

  • sudo apt update refreshes the list of available packages from Ubuntu's repositories.
  • sudo apt upgrade -y installs all available updates. The -y flag automatically confirms the installation.

You'll see a stream of output as packages download and install. This may take a minute or two depending on how many updates are available.

Seeing a dpkg lock error?

If you see Could not get lock /var/lib/dpkg/lock-frontend, Cloud-Init is still running its initial package updates in the background. Wait a minute and try again.

help me fix the dpkg lock error on my Docker host VM

  • Install and enable the QEMU guest agent by running these commands:
sudo apt install -y qemu-guest-agent
sudo systemctl enable --now qemu-guest-agent

What is the QEMU guest agent?

The QEMU guest agent is a small service that runs inside the VM and communicates with Proxmox. It lets Proxmox see the VM's IP address, gracefully shut down the VM, and freeze the filesystem for clean backups.

The enable --now flag both enables the service to start on boot and starts it immediately.

You can verify the agent is running by checking its status.

  • Confirm the guest agent is active by running:
sudo systemctl status qemu-guest-agent

You should see Active: active (running) in the output. Press q to exit the status view.

Your Docker host VM is cloned, updated, and ready to go. Next up, you'll install Docker Engine inside this VM and pull your first container image.

Install Docker Engine

Your Docker host VM is cloned and fully updated. You're SSH'd in and ready to go.

The VM is a blank Ubuntu machine right now. You'll install Docker Engine to turn it into a container host.

In this step, get ready to:
  • Learn what Docker is and how containers differ from virtual machines.
  • Install Docker Engine using Docker's official convenience script.
  • Verify the installation by pulling and running your first container image.
Understand what Docker is

Docker is a platform that packages applications into containers. You built a full VM in the previous project, so you already know how to isolate an operating system on your server.

What is a container?

  • A container bundles an app with everything it needs to run: code, libraries, and system tools.
  • Containers share the host's Linux kernel instead of running a full operating system. This makes them start in seconds.
  • Your single Docker host VM can run dozens of containers at once, each completely isolated from the others.

Docker has two main components that work together.

How does Docker work?

  • The Docker daemon (dockerd) is a background service that manages containers, images, and networks.
  • The Docker CLI (docker) is the command-line tool you use to talk to the daemon.
  • When you run a command like docker run, the CLI sends it to the daemon, which does the actual work.
Install Docker using the official convenience script

Docker's official convenience script installs Docker Engine with a single command. It detects your Linux distribution and handles all the setup automatically.

Why the convenience script instead of the manual method?

Docker offers two install methods: adding the repository manually with apt, or running the official convenience script. The convenience script handles everything in one command. It's designed for development environments and learning projects like this one.

  • Download and install Docker Engine by running this command:
curl -fsSL https://get.docker.com | sh

What does this command do?

  • curl -fsSL https://get.docker.com downloads Docker's official install script from the internet.
  • The | pipe sends the downloaded script directly to sh, which runs it immediately.
  • The script detects that you're running Ubuntu 24.04, adds Docker's package repository, and installs Docker Engine.

The script takes a minute or two to run. When it finishes, you should see a message confirming that Docker Engine is installed.

Installation failed?

  • Make sure you're connected to the internet inside the VM. Try running ping google.com to check.
  • If you see a permissions error, make sure you're running the command as the ubuntu user (not root). The script uses sudo internally.
  • Still stuck? Help me troubleshoot the Docker install script failing on my Ubuntu 24.04 VM.

Docker is installed, but right now you need sudo to run any Docker command. Adding your user to the docker group fixes this.

  • Add your user to the docker group by running:
sudo usermod -aG docker ubuntu
  • Log out of the SSH session by running:
exit
  • SSH back into your VM to apply the group change by running:
ssh ubuntu@[[VM_IP="your VM's IP address"]]

Why do I have to log out and back in?

Linux group changes only take effect when you start a new session. Logging out and SSH-ing back in creates a fresh session where your user picks up the new docker group membership.

You should see the Ubuntu command prompt again, confirming you're back inside the VM.

Can't reconnect via SSH?

  • Double-check the VM's IP address in the Proxmox Summary tab. It may have changed after the reboot.
  • Make sure the VM is still running in the Proxmox web UI. If it shows as stopped, click Start and wait a minute before trying again.
  • Still stuck? Help me reconnect to my Ubuntu VM via SSH.
Verify Docker is running and pull your first image

Time to confirm that Docker Engine is working and that your user can run Docker commands without sudo.

  • Check Docker's version by running:
docker --version

You should see a version number like 29.4.0 in the output.

  • Pull and run your first container by running:
docker run hello-world

What just happened?

That single command triggered a chain of events inside Docker.

  • Docker looked for the hello-world image on your machine and didn't find it.
  • It pulled the image from Docker Hub, Docker's public registry of container images.
  • Docker created a new container from the image and ran it.
  • The container printed a success message and exited.

You should see Hello from Docker! followed by a paragraph explaining that your installation appears to be working correctly.

Seeing 'permission denied'?

  • This means the group change from the previous substep didn't take effect. Make sure you logged out with exit and SSH'd back in.
  • Confirm your user is in the docker group by running groups. You should see docker in the output list.
  • If docker is missing from the group list, run sudo usermod -aG docker ubuntu again, then exit and SSH back in.
  • Still stuck? Help me fix Docker permission denied errors on Ubuntu.

Docker Engine is installed and working. Next up, you'll deploy Portainer as your first real container and access its web dashboard from your iPad.

Deploy Portainer as Your First Real Container

Docker Engine is installed and running inside your VM. You confirmed it works by pulling and running the hello-world container.

Running containers from the command line works, but a visual dashboard makes Docker much easier to manage. In this step, you'll deploy Portainer CE, a web-based Docker management UI that runs as a container itself.

In this step, get ready to:
  • Create a Docker volume to persist Portainer's data.
  • Deploy the Portainer CE container.
  • Verify Portainer is running with docker ps.
Create a Docker volume for Portainer's data

Containers are temporary by default. When you remove a container, everything inside it disappears.

What is a Docker volume?

A volume is a storage area managed by Docker that lives outside the container's filesystem. Even if you delete and recreate the container, data stored in its volume survives.

The volume you're about to create stores Portainer's configuration, user accounts, and settings.

  • Create a volume named portainer_data by running this command:
docker volume create portainer_data

You should see portainer_data printed in your terminal, confirming the volume was created.

Run the Portainer container

With the volume ready, you can deploy Portainer as a container. This single command pulls the Portainer image, creates a container from it, and starts it in the background.

  • Deploy Portainer by running this command:
docker run -d \
  --name portainer \
  --restart=always \
  -p 8000:8000 \
  -p 9443:9443 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v portainer_data:/data \
  portainer/portainer-ce:2.38.1

What does this command do?

Each flag controls how the container runs:

  • -d runs the container in the background (detached mode) so it doesn't lock up your terminal.
  • --name portainer gives the container a friendly name instead of a random one.
  • --restart=always tells Docker to automatically restart the container whenever the VM reboots.
  • -p 9443:9443 maps port 9443 on the VM to port 9443 inside the container. This is how you'll access Portainer's HTTPS web UI.
  • -p 8000:8000 maps port 8000 for Edge Agent communication (used in advanced setups).
  • -v /var/run/docker.sock:/var/run/docker.sock gives Portainer access to Docker's API. The Docker socket is how programs talk to the Docker daemon. Without this, Portainer can't see or manage your containers.
  • -v portainer_data:/data mounts the volume you created so Portainer's data persists across restarts.
  • portainer/portainer-ce:2.38.1 is the container image to pull from Docker Hub. The tag 2.38.1 pins it to a specific version for consistency.

Docker prints a long string of characters. This is the container ID, confirming the container was created.

Verify Portainer is running
  • Check that Portainer is running by running this command:
docker ps

You should see the portainer container listed with a status of Up. The PORTS column shows 8000 and 9443 mapped, confirming Portainer is ready to accept connections.

What does docker ps show?

The docker ps command lists all running containers. Each row shows the container's ID, the image it was created from, how long it's been running, its port mappings, and its name.

  • Note the VM's IP address from the Proxmox Summary tab. You'll need it in the next step to access Portainer's web UI from your iPad.

Don't see the portainer container?

  • The container might have started and then stopped. Run docker ps -a to see all containers, including stopped ones. If portainer shows with status Exited, check the logs with docker logs portainer.
  • If you see a "port already in use" error, another service is using port 8000 or 9443. Remove the failed container with docker rm portainer, then rerun the docker run command.
  • If the image failed to download, check that your VM has internet access by running ping google.com.

Help me troubleshoot why my Portainer container isn't running after docker run

Portainer is deployed and running inside your VM. Next up, you'll open Portainer's web dashboard from your iPad and set up your admin account.

Access Portainer from Your iPad on the Local Network

Your Portainer container is running inside your VM. Right now it is serving a web dashboard, but nobody has logged in yet.

In this step, you will open that dashboard from your iPad's browser, create your admin account, and take your first look at the Docker management interface you will use for the rest of the project.

In this step, get ready to:
  • Connect to Portainer's web UI from Safari on your iPad.
  • Create your admin account and connect Portainer to your local Docker environment.
  • Explore the Portainer dashboard and see your Docker resources at a glance.
Connect from your iPad

Portainer is listening on port 9443 inside your VM, serving its dashboard over HTTPS. As long as your iPad is on the same local network as your Proxmox server, you can reach it directly from Safari.

  • On your iPad, open Safari.
  • In the address bar, type the following URL (replacing the IP with your Docker host VM's IP address from the Proxmox Summary tab):
https://[[VM_IP="your Docker host VM's IP address"]]:9443

Safari displays a "This Connection Is Not Private" warning page. This is expected.

Why am I seeing a privacy warning?

Portainer generates its own SSL certificate when it starts up. This certificate encrypts your connection, but no trusted certificate authority signed it. Safari does not recognize the certificate and warns you. Since this is your own server on your own network, it is safe to proceed.

  • Tap Show Details on the warning page.
  • Tap visit this website at the bottom of the details section.
  • A confirmation popup appears. Tap Visit Website to proceed.

You should now see the Portainer initial setup screen with a form to create your first user.

  • The Username field defaults to admin. You can keep this or change it to a name you prefer.
  • Enter a strong password in the Password field. It must be at least 12 characters long.
  • Confirm your password in the second password field.
  • Click Create user.

Pro tip: remember your password

Write down or save the username and password somewhere safe. You will need these credentials every time you log in to Portainer.

Can't reach Portainer?

  • Double-check that you typed https (not http) and port 9443 in the URL.
  • Make sure your iPad is connected to the same Wi-Fi network as your Proxmox server.
  • Confirm the VM is still running by checking its status in the Proxmox web UI.
  • Verify the IP address matches what the Proxmox Summary tab shows for your docker-host VM.
  • Still stuck? Help me troubleshoot why I can't reach Portainer from my iPad.
Connect Portainer to your local Docker environment

After creating your admin account, Portainer launches its Environment Wizard. This wizard asks which Docker environment you want to manage.

  • Click Get Started to connect to the local Docker environment.

What just happened?

Portainer automatically detected the Docker Engine running on the same host. Remember the -v /var/run/docker.sock:/var/run/docker.sock flag you used when deploying Portainer? That gave Portainer direct access to Docker's API. It uses this connection to discover and manage all containers, images, volumes, and networks on the host.

Portainer loads its home screen showing your connected environment. You should see an environment called local listed on the dashboard.

Explore the Portainer dashboard

The home screen shows your environments. Clicking into one opens the full management view where you can control every aspect of Docker on that host.

  • Click on the local environment to enter the management view.

You should see a summary dashboard showing the number of running containers, images, volumes, and networks. The running container count shows 1. That single container is Portainer itself.

  • Look at the left sidebar menu. You will see sections including Containers, Images, Volumes, Networks, and Stacks.
  • Click Containers in the sidebar to see the list of running containers.

You should see the portainer container listed with a green "running" status. This is the same container you deployed from the terminal in the previous step.

  • Click Images in the sidebar to see the Docker images stored on your host.
  • Click Volumes to see the portainer_data volume you created earlier.
  • Click Networks to see the Docker networks available on your host.

What are all these sections for?

  • Containers shows every container on the host, whether running or stopped. You can start, stop, restart, and remove containers from here.
  • Images shows the Docker images downloaded to your host. Images are the blueprints that containers are created from.
  • Volumes shows persistent storage that survives container removal. Your Portainer data lives here.
  • Networks shows the virtual networks that containers use to communicate with each other.
  • Stacks is where you deploy multi-container applications using Docker Compose. You will use this in a later step.

This dashboard is your Docker command center. Everything you would normally do with terminal commands, you can now do from your iPad's browser.

Your Portainer dashboard is set up and connected. Next up, you will use it to pull a Docker image, create a web server container, and practice starting, stopping, and removing it.

Pull Images, Create Containers, and Manage the Lifecycle

You've explored the Portainer dashboard and know where everything lives. Now it's time to put it to work.

In this step, you'll pull a Docker image, create a web server container from it, and practice the full container lifecycle.

In this step, get ready to:
  • Pull the Nginx image and deploy it as a web server container through Portainer.
  • Verify your container is serving a live web page from your iPad.
  • Practice starting, stopping, and removing a container.
Pull the Nginx image and create a container

Every Docker container starts from an image. You'll use Portainer to pull the Nginx web server image and create a running container from it.

What is a Docker image?

An image is a read-only template that bundles an application with everything it needs to run. Docker Hub is a public registry with thousands of pre-built images available for free. When you "pull" an image, you download it from Docker Hub to your local Docker Engine.

A container is a running instance of an image. You can create many containers from the same image, just like printing many copies of the same blueprint.

  • In Portainer, click Containers in the left sidebar.
  • Click Add container.
  • In the Name field, enter my-web-server.
  • In the Image field, type nginx:latest.
  • Scroll down to Network ports configuration.
  • Click publish a new network port.
  • Enter 8080 in the host field.
  • Enter 80 in the container field.
  • Scroll down and click Deploy the container.

Portainer pulls the Nginx image from Docker Hub and creates a running container. You should see my-web-server appear in the container list with a green Running status.

What is port mapping?

Port mapping connects a port on your VM to a port inside the container. Nginx listens on port 80 inside the container, but your iPad can't reach that port directly.

By mapping host port 8080 to container port 80, any traffic your iPad sends to port 8080 on the VM gets forwarded into the container where Nginx is listening.

Verify your web server is running

Your container is running inside the VM. Time to confirm Nginx is actually serving web pages.

  • Open a new tab in Safari on your iPad.
  • Enter your VM's IP address on port 8080 in the address bar, formatted like this:
http://[[VM_IP="your VM's IP address"]]:8080

You should see the default Nginx welcome page. It displays a heading that reads Welcome to nginx! in your browser.

Not seeing the Nginx page?

  • Double-check that you entered the correct IP address for your VM. You can find it in the Proxmox Summary tab for VM 200.
  • Make sure you typed http:// (not https://) in the address bar. Nginx is serving plain HTTP on port 8080.
  • Go back to Portainer and check the Containers list. If my-web-server does not show a green Running status, remove it and repeat the steps in the previous substep.
  • Help me troubleshoot why I can't see the Nginx welcome page on my iPad.
Manage the container lifecycle

Containers can be running, stopped, or removed entirely. Portainer gives you controls for each of these states on the container details page.

  • In Portainer, click on the container name my-web-server to open its details page.
  • Click Logs to view the container's output.
  • Click Stats to see live CPU and memory usage.

What do logs and stats show?

Logs show everything the application prints while running. For Nginx, you'll see each incoming web request logged with the visitor's IP address, the URL requested, and the response code.

Stats show real-time resource consumption. You can monitor how much CPU and memory your container is using at any moment.

Now test what happens when a container stops.

  • Click the Stop button at the top of the container details page.
  • Switch to the Safari tab where your Nginx welcome page is open.
  • Refresh the page.

The page fails to load. Stopping a container shuts down the application running inside it.

  • Switch back to the Portainer tab in Safari.
  • Click Start to bring the container back up.
  • Switch to the Nginx tab in Safari.
  • Refresh the page again.

The Nginx welcome page is back. Starting a stopped container resumes the application from where it left off.

Finally, clean up by removing the container.

  • Switch back to the Portainer tab.
  • Click Stop to stop the container.
  • Click Remove to delete the container.

What happens when you remove a container?

Removing a container deletes it permanently. Any data stored inside the container is lost.

The Nginx image stays downloaded on your Docker host. You can create new containers from it anytime without pulling it again.

Can't click Remove?

The Remove button is only available when a container is stopped. Make sure you click Stop first before trying to remove it.

Help me remove a container in Portainer.

You've created a container, watched it serve live web traffic, and managed its full lifecycle from your iPad. Next up, you'll deploy a multi-container application with a database backend using Docker Compose.

Deploy a Multi-Container Stack with Docker Compose

In the last step, you pulled an image, created a container, and managed its full lifecycle from Portainer on your iPad. You can now start, stop, and remove individual containers with confidence.

Real applications rarely run as a single container, though. A typical web app needs a web server and a database at minimum. Docker Compose lets you define multiple containers in one file and launch them together. In this step, you will deploy a full WordPress site backed by a MySQL database.

In this step, get ready to:
  • Learn what Docker Compose and Stacks are.
  • Deploy a WordPress site with a MySQL database through Portainer.
  • Access your new WordPress site from your iPad.
Understand Docker Compose and Stacks

So far, every container you created was a standalone unit. You ran one docker run command for Portainer, and you created one Nginx container through the Portainer UI.

A WordPress site needs two containers working together: one running the WordPress application, and one running the MySQL database that stores all the content. These containers need to share a private network so WordPress can talk to MySQL. Setting this up manually with individual docker run commands would be tedious and error-prone.

What is Docker Compose?

Docker Compose lets you define a multi-container application in a single YAML file. Instead of running multiple commands, you describe all your containers, their settings, networks, and volumes in one place. Docker Compose reads that file and creates everything for you.

A Stack in Portainer is Docker Compose with a visual interface. You paste the YAML into Portainer's web editor, click deploy, and Portainer handles the rest.

Create the WordPress stack
  • In Portainer on your iPad, click Stacks in the left sidebar menu.
  • Click Add stack.
  • In the Name field, type wordpress.
  • In the Web editor section below, paste the following Docker Compose YAML:
version: "3.8"

services:
  # MySQL database that stores WordPress content
  db:
    image: mysql:8.0
    container_name: wordpress-db
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword123
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wpuser
      MYSQL_PASSWORD: wppassword123
    volumes:
      - db_data:/var/lib/mysql

  # WordPress web application
  wordpress:
    image: wordpress:latest
    container_name: wordpress-app
    restart: always
    depends_on:
      - db
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wpuser
      WORDPRESS_DB_PASSWORD: wppassword123
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - wp_data:/var/www/html

# Named volumes keep data safe when containers are removed
volumes:
  db_data:
  wp_data:

What does this YAML do?

This file defines two services that work together as one application:

  • db pulls the mysql:8.0 image and creates a database container. The environment section sets up the database name, username, and password that WordPress will use to connect.
  • wordpress pulls the wordpress:latest image and connects it to the database. The depends_on field tells Docker to start the database first.
  • ports: "8080:80" maps port 8080 on your VM to port 80 inside the WordPress container. This is the same port mapping pattern you used with Nginx.
  • volumes at the bottom creates two named volumes. These keep your database data and WordPress files safe even if the containers are removed.

Pro tip: change these passwords

The YAML above uses simple placeholder passwords like rootpassword123. These are fine for learning, but if you ever deploy WordPress for real use, replace them with strong, unique passwords before deploying.

  • Scroll down and click Deploy the stack.

Portainer pulls both images from Docker Hub, creates the containers, sets up a private network between them, and mounts the volumes. This may take a minute or two.

  • Wait for the page to update. You should see the stack status change to Active with two running containers listed below it.

Stack stuck or showing errors?

  • If you see a port conflict error, the Nginx container from the previous step may still be using port 8080. Go to Containers in the left menu, find any container using port 8080, stop it, and remove it. Then try deploying the stack again.
  • If the deployment takes more than two minutes, click on the stack name to check individual container statuses. Click on a container name, then click Logs to see what went wrong.
  • Double check that the YAML is pasted correctly. A single indentation error in YAML will cause the deployment to fail.

Help me troubleshoot my WordPress stack deployment in Portainer.

Access your WordPress site

Your WordPress and MySQL containers are now running together. WordPress is listening on port 8080 of your VM, just like Nginx was in the last step.

  • Open a new tab in Safari on your iPad.
  • Navigate to your WordPress site by entering this URL:
http://[[VM_IP="your Docker host VM's IP address"]]:8080

You should see the WordPress setup wizard asking you to select a language.

  • Select your preferred language and click Continue.
  • Fill in a site title, username, and password for your WordPress admin account.
  • Click Install WordPress.

After the install finishes, you will see the WordPress dashboard. You just deployed a full web application with a database backend from your iPad.

What just happened behind the scenes?

When you typed that URL, your iPad's browser sent a request to port 8080 on your VM. Docker forwarded that request to port 80 inside the WordPress container. WordPress then connected to the MySQL container over a private Docker network to read and write data. Two containers, working as one application.

Seeing a connection error instead of WordPress?

  • Make sure you are using http:// and not https:// in the URL. WordPress runs on plain HTTP on port 8080.
  • Go back to Portainer and click Containers in the left menu. Confirm that both wordpress-app and wordpress-db show a status of Running.
  • The MySQL container can take up to 30 seconds to fully initialize. If both containers are running but WordPress shows a database connection error, wait 30 seconds and refresh the page.

Help me connect to my WordPress site running in Docker.

Your multi-container stack is live. Next up, you will install Tailscale so you can access Portainer and your containers from anywhere in the world, not just your local network.

Set Up Tailscale for Remote Access from Anywhere

In the last step, you deployed a full WordPress site with a MySQL database from your iPad. You now have a complete Docker management dashboard running on your Proxmox server.

There is one big limitation though. Your iPad can only reach Portainer when both devices are on the same Wi-Fi network. Step outside your house, and you lose access. In this step, you will install Tailscale to connect your iPad to your Docker dashboard from anywhere in the world.

In this step, get ready to:
  • Create a Tailscale account and install Tailscale on your Docker host VM.
  • Install Tailscale on your iPad and join the same network.
  • Access Portainer from anywhere in the world through an encrypted Tailscale tunnel.
Create a Tailscale account and install on your VM

Right now, your iPad connects to Portainer using a local IP address that only works inside your home network. Tailscale creates an encrypted tunnel between your devices so they can reach each other over the internet.

What is Tailscale?

Tailscale is a mesh VPN. It connects your devices into a private network where each device gets a stable IP address in the 100.x.x.x range. Every connection between devices is encrypted end-to-end.

Unlike traditional VPNs, you do not need to open any router ports or configure a VPN server. You install Tailscale on each device, sign in with the same account, and the devices can reach each other from anywhere.

Why Tailscale instead of WireGuard?

Tailscale uses WireGuard under the hood, but handles all the complexity for you. With raw WireGuard, you would need to generate keys, exchange them between devices, and open a port on your router. Tailscale automates all of that.

Another option is Cloudflare Tunnel, but that requires owning a domain name and editing DNS records. Tailscale is install-and-go.

  • On your Mac or iPad, open a browser and go to https://login.tailscale.com.
  • Click Sign Up and create a free account using Google, Microsoft, or GitHub.

With your Tailscale account created, it is time to install Tailscale on your Docker host VM.

  • SSH into your Docker host VM from your Mac by running this command:
ssh ubuntu@[[VM_IP="your VM's IP address"]]
  • Install Tailscale by running this command:
curl -fsSL https://tailscale.com/install.sh | sh

What does this command do?

This downloads Tailscale's official install script and pipes it directly into your shell. The script detects your Linux distribution and installs the correct Tailscale package automatically.

  • You should see output ending with a message confirming Tailscale was installed successfully.

Seeing a connection error?

  • Make sure your VM has internet access. Try running ping google.com to check.
  • If the script fails partway through, run the same command again. Network timeouts sometimes resolve on retry.
  • Help me troubleshoot the Tailscale install script failing on Ubuntu 24.04.

Now you need to start Tailscale and connect your VM to your Tailscale account. This command will print a URL in the terminal that you need to open in a browser.

  • Start Tailscale by running this command:
sudo tailscale up

What's happening here?

The command prints a line that looks like:

To authenticate, visit: https://login.tailscale.com/a/abc123...

This URL links your VM to your Tailscale account. The terminal will wait until you complete the authentication in your browser.

  • Copy the URL from the terminal output.
  • Open the URL in a browser on your Mac or iPad.
  • Sign in with the same Tailscale account you just created.
  • Click Connect to authorize the VM.

Your terminal should return to the command prompt, confirming the VM is now connected to your Tailscale network.

  • Get your VM's Tailscale IP address by running this command:
tailscale ip -4
  • You should see an IP address starting with 100. (for example, 100.64.0.1). Write this IP address down.

Pro tip: disable key expiry

Tailscale device keys expire after 180 days by default. For a server that should stay connected permanently, disable key expiry.

Install Tailscale on your iPad

Your VM is on the Tailscale network. Now you need to add your iPad to the same network so both devices can find each other.

  • On your iPad, open the App Store.
  • Search for Tailscale.
  • Tap Get to install the app.
  • Open the Tailscale app and sign in with the same account you used for your VM.

Your iPad and your Docker host VM are now on the same Tailscale network. Both devices can reach each other over an encrypted connection, no matter where they are in the world.

Don't see your VM in the Tailscale app?

  • Make sure you signed in to the iPad app with the exact same account you used to authenticate the VM. Different accounts create separate networks.
  • Check that the Tailscale toggle inside the iPad app is switched on.
  • My iPad's Tailscale app can't see my Docker host VM. Help me debug the connection.
Access Portainer from anywhere using Tailscale

This is the moment of truth. Instead of using your VM's local IP address, you will use the Tailscale IP to reach Portainer. This connection works from any network in the world.

  • On your iPad, open Safari.
  • Navigate to the following URL, replacing the IP with your VM's Tailscale IP from the previous substep:
https://[[TAILSCALE_IP="your VM's Tailscale IP (100.x.x.x)"]]:9443
  • Accept the self-signed certificate warning. This is the same warning you saw when first accessing Portainer over your local network.

You should see the Portainer login page. This is the exact same dashboard you have been using, but now accessed through an encrypted Tailscale tunnel.

  • Log in with the admin username and password you created in Step 4.
  • Verify that you can see all your containers, stacks, and volumes in the dashboard.

Now for the real test. Try accessing Portainer from outside your home network.

  • On your iPad, open Settings.
  • Tap Wi-Fi and toggle it off to disconnect from your home network.
  • Make sure your iPad is using cellular data (or a mobile hotspot from your phone).
  • Go back to Safari and refresh the Portainer dashboard.

Portainer loads just like before. You are now managing your Docker containers from a completely different network, with no port forwarding and no router configuration. The Tailscale tunnel handles everything.

Portainer not loading over Tailscale?

  • Double check that you are using the Tailscale IP (100.x.x.x), not your VM's local IP address.
  • Make sure the Tailscale app on your iPad is toggled on and shows a Connected status.
  • Confirm the Portainer container is still running on your VM. SSH in and run docker ps to check.
  • Make sure you include https:// and port 9443 in the URL. Portainer uses HTTPS on port 9443, not HTTP on port 80.
  • I can't access Portainer through my Tailscale IP. Help me debug the connection.

Secret mission

Manage Multiple Docker Hosts from One Dashboard

You have one Portainer dashboard managing one Docker host. Real infrastructure runs across multiple servers. In this mission, you'll deploy a second Docker host VM and connect it to your existing Portainer instance using the Portainer Agent.

Before you go

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely on your local Proxmox server, so there are no ongoing cloud costs. The Tailscale free tier is sufficient for personal use.

Resources you used:

  • Docker host VM (ID 200) running on Proxmox.
  • Portainer CE container running inside the VM.
  • Nginx and WordPress/MySQL containers.
  • Tailscale free account with two devices (VM and iPad).
  • Secret Mission: second Docker host VM (ID 201) with Portainer Agent.

Keep everything running

No action needed. Choose this if you are still actively building or want your Docker dashboard available on your iPad at all times.

  • Leave the Docker host VM (ID 200) running in Proxmox.
  • Portainer and your containers will keep running inside the VM.
  • Your Docker management dashboard stays accessible from your iPad anytime via Tailscale.

Pause - I'll come back to this later

Shut down the VM to free up memory on your Proxmox server. All containers and data persist on disk.

  • In the Proxmox web UI, select VM 200 and click Shutdown.
  • If you completed the Secret Mission, also shut down VM 201.
  • When you start the VM again, Portainer and your containers will auto-start because you used --restart=always.

Delete - I don't want to use this again

Remove all project resources and start fresh. Your VM template (ID 9000) remains untouched for future projects.

  • In the Proxmox web UI, select VM 200, click Stop, then right-click and select Remove.
  • If you completed the Secret Mission, also stop and remove VM 201.
  • Remove your Tailscale devices from the Tailscale admin console at https://login.tailscale.com/admin/machines.
  • Uninstall the Tailscale app from your iPad if you no longer need it.

Mission accomplished

That's a Wrap!

Nice work! You just turned your iPad into a Docker command center that works from anywhere in the world.

You've learned how to:

  • Installed Docker Engine on a dedicated VM and deployed Portainer CE as a web-based dashboard for managing containers from your iPad's browser.
  • Pulled images, created an Nginx web server container, and managed the full container lifecycle (start, stop, remove) through Portainer.
  • Deployed a full WordPress + MySQL multi-container stack using Docker Compose and set up Tailscale for remote access from anywhere in the world.
  • Deployed a Portainer Agent on a second Docker host and managed multiple hosts from a single dashboard on your iPad.

Ready to quiz yourself?