Using Docker for Self-Hosting
Before Docker, installing a self-hosted application meant manually setting up a web server, a database, a programming language runtime, and dozens of library dependencies, all of which had to be compatible versions that did not conflict with anything else on the system. Adding a second application often meant fighting with conflicting dependency requirements. Docker solved this problem by packaging each application and all its dependencies into a self-contained unit called a container.
Understand Docker Concepts
Docker uses a handful of core concepts that, once understood, make everything else click into place.
An image is a read-only template containing an application and its dependencies. Think of it like a snapshot of a preconfigured operating system with the application already installed. Images are built by the application developers and published to a registry, usually Docker Hub. When you deploy Nextcloud or Jellyfin with Docker, you download their official image from Docker Hub.
A container is a running instance of an image. When you start a container, Docker creates an isolated process with its own filesystem, network interface, and process space. Containers share the host machine's kernel, so they start in seconds and use far less memory than virtual machines. You can run multiple containers from the same image, and each one operates independently.
A volume is persistent storage that survives when a container is stopped or removed. By default, any data written inside a container is lost when the container is deleted. Volumes solve this by mapping a directory on the host filesystem into the container. Your application's database, configuration files, and user data should always be stored in volumes. This separation between the disposable container and the persistent data is what makes updates safe: you replace the container with a new version, and your data remains untouched in the volume.
A network connects containers together so they can communicate. Docker creates isolated networks that keep container traffic separate from the host network and from other container groups. When two containers need to talk to each other, such as a web application and its database, they join the same Docker network and reference each other by container name instead of IP address.
Install Docker and Docker Compose
Docker Engine and the Compose plugin install together on Ubuntu and Debian using the official convenience script. Run curl -fsSL https://get.docker.com | sudo sh and the script handles adding the Docker repository, installing the packages, and starting the Docker service. After installation, add your user to the docker group with sudo usermod -aG docker $USER, then log out and log back in for the change to take effect.
Verify the installation by running docker --version and docker compose version. Both commands should return version numbers. Running docker run hello-world confirms that Docker can pull images and start containers successfully.
Avoid installing Docker from your Linux distribution's default package repository (like apt install docker.io) because these packages are often outdated. The official Docker repository provides the latest stable releases with security patches and new features.
Write Your First Compose File
Docker Compose lets you define your entire application stack in a single YAML file called docker-compose.yml. This file specifies which images to use, what ports to expose, where to mount volumes, and what environment variables to set. The advantage over running individual docker run commands is that your configuration is documented, version-controlled, and reproducible.
A typical Compose file for a self-hosted application has three to five key sections per service. The image field specifies the Docker image and tag to use. The ports section maps host ports to container ports so you can access the application from your browser. The volumes section maps host directories to container paths for persistent data. The environment section provides configuration variables like database credentials, timezone settings, and application-specific options. The restart policy, usually set to unless-stopped, ensures the container starts automatically after a server reboot.
Create a dedicated directory for each application's Compose file. A common convention is /opt/stacks/appname/docker-compose.yml or ~/docker/appname/docker-compose.yml. This organization makes it easy to manage multiple services independently. Each application gets its own Compose file, its own volumes, and can be started, stopped, and updated without affecting other services.
When a service requires a database (as most web applications do), include the database as a second service in the same Compose file. The application and database containers share a Docker network defined in the Compose file, allowing the application to connect to the database by its service name. This keeps the database private and inaccessible from outside the Docker network unless you explicitly expose it.
Manage Containers
Day-to-day Docker management uses a small set of commands that you will quickly memorize.
docker compose up -d starts all services defined in the Compose file in the background (detached mode). Run this from the directory containing the Compose file. Docker creates containers, networks, and volumes as needed and starts the application.
docker compose down stops and removes the containers and networks but preserves the volumes and your data. Use this when you want to stop a service cleanly. Adding the -v flag also removes volumes, which permanently deletes all data, so use -v only when you intentionally want to wipe everything and start fresh.
docker compose logs -f follows the log output from all services in the Compose file. This is your primary troubleshooting tool. When something is not working, the logs almost always contain the error message that explains why. Add a service name to the end to filter logs to a specific container.
docker compose ps shows the status of all containers in the current Compose project, including whether they are running, their uptime, and their port mappings.
docker exec -it container_name sh opens a shell inside a running container, allowing you to inspect files, check configurations, or run commands within the container's environment. Replace sh with bash if the container includes bash.
Configure Networking
Docker Compose automatically creates a network for each Compose project, and all services in the same file can communicate using their service names as hostnames. This default behavior works well for most self-hosted setups.
When you need containers from different Compose files to communicate, create a shared external network. Define the network with docker network create shared, then reference it in each Compose file's networks section with external: true. A common use case is a reverse proxy like Caddy or Nginx Proxy Manager running in its own Compose file but routing traffic to applications defined in other Compose files. All the applications join the shared network so the reverse proxy can reach them by service name.
Only expose ports that need to be accessible from outside the Docker network. If a database is only used by the application in the same Compose file, it does not need a published port. If an application is only accessed through a reverse proxy, you can remove its port mapping entirely and let the proxy reach it through the Docker network. Minimizing exposed ports reduces your attack surface.
Handle Updates
Updating a Docker-based application is a three-step process that typically completes in under a minute. First, pull the latest image with docker compose pull. This downloads the new image without affecting the running container. Second, recreate the containers with docker compose up -d. Docker detects that the image has changed, stops the old container, and starts a new one with the updated image. Your volumes remain intact, so all your data and configuration persist across the update. Third, optionally clean up old images with docker image prune to free disk space.
Before updating, check the application's release notes or changelog for any breaking changes or required migration steps. Most updates are seamless, but major version upgrades occasionally require running a migration command or updating the Compose file format. Keeping a backup of your volumes before a major update provides a safety net in case something goes wrong.
For automated updates, Watchtower monitors your running containers and automatically pulls new images on a schedule. It can be configured to update all containers or only specific ones, and it supports notifications through email, Slack, Discord, and other channels. Some self-hosters prefer manual updates so they can review changelogs before applying changes, while others appreciate the hands-off approach of Watchtower for non-critical services.
Use Management Tools
While Docker Compose and the command line are sufficient for managing any self-hosted setup, web-based management interfaces provide a visual overview that is especially helpful when running many services.
Portainer is the most feature-rich option. It provides a web dashboard for managing containers, images, volumes, networks, and stacks. You can start, stop, and restart containers, view logs, inspect container details, and even deploy new stacks from the Portainer interface. Portainer itself runs as a Docker container and adds overhead of roughly 50 to 100 MB of RAM.
Dockge is a newer, lighter alternative focused specifically on Docker Compose management. It lets you create, edit, and manage Compose files through a clean web interface, with syntax highlighting and validation for the YAML configuration. Dockge does not try to be a full container management platform; it focuses on making Compose-based workflows more visual and accessible.
These tools are optional and add convenience rather than capability. Everything they do can also be done from the command line. Choose one if you prefer a graphical interface or if you want an easy way to check on your services without opening a terminal.
Docker Compose is the backbone of modern self-hosting. Learn the four core concepts (images, containers, volumes, networks), write a Compose file for each application, and use docker compose up -d and docker compose down to manage your services. Everything else builds on these fundamentals.